From 95fb998ad30d584e7178dcac35a45637f4d27c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 13 Jan 2026 11:10:21 +0100 Subject: [PATCH 01/52] feat: new typealiases --- Sources/Container/Async/AsyncContainer.swift | 2 +- Sources/Container/Sync/Container.swift | 2 +- .../Async/AsyncDependencyRegistering.swift | 12 +++++++++--- .../Sync/DependencyWithArgumentAutoregistering.swift | 12 ++++++------ .../Sync/DependencyWithArgumentRegistering.swift | 6 +++--- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index bbc51c2..3808f68 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -70,7 +70,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// - 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 FactoryWithOneArgument) async { let registration = AsyncRegistration(type: type, scope: .new, factory: factory) registrations[registration.identifier] = registration diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 3903dff..fcfb913 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -66,7 +66,7 @@ open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregis /// - 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 FactoryWithOneArgument) { let registration = Registration(type: type, scope: .new, factory: factory) registrations[registration.identifier] = registration diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index fe68a8d..5d2b4c6 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -13,7 +13,13 @@ public protocol AsyncDependencyRegistering { 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 + typealias FactoryWithOneArgument = @Sendable (any AsyncDependencyResolving, Argument) async -> Dependency + + /// Factory closure that instantiates the required dependency with two variable arguments + typealias FactoryWithTwoArgument = @Sendable (any AsyncDependencyResolving, Argument1, Argument2) async -> Dependency + + /// Factory closure that instantiates the required dependency with three variable arguments + typealias FactoryWithThreeArgument = @Sendable (any AsyncDependencyResolving, Argument1, Argument2, Argument3) async -> Dependency /// Register a dependency /// @@ -37,7 +43,7 @@ public protocol AsyncDependencyRegistering { /// - 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 FactoryWithOneArgument) async } // MARK: Overloaded factory methods @@ -88,7 +94,7 @@ public extension AsyncDependencyRegistering { /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping FactoryWithArgument) async { + func register(factory: @escaping FactoryWithOneArgument) async { await register(type: Dependency.self, factory: factory) } } diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift index 3df128e..375c389 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift @@ -168,7 +168,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Argument) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( argument ) @@ -202,7 +202,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Argument, Parameter) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( argument, resolver.resolve(type: Parameter.self) @@ -230,7 +230,7 @@ public extension DependencyWithArgumentAutoregistering { /// - 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 + let factory: FactoryWithOneArgument = { resolver, argument in initializer( resolver.resolve(type: Parameter.self), argument @@ -265,7 +265,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Argument, Parameter1, Parameter2) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( argument, resolver.resolve(type: Parameter1.self), @@ -298,7 +298,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Parameter1, Argument, Parameter2) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), argument, @@ -331,7 +331,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Parameter1, Parameter2, Argument) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), resolver.resolve(type: Parameter2.self), diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift index 08f0951..9c1d69c 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift @@ -10,7 +10,7 @@ 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 + typealias FactoryWithOneArgument = (DependencyWithArgumentResolving, Argument) -> Dependency /// Register a dependency with a variable argument /// @@ -26,7 +26,7 @@ public protocol DependencyWithArgumentRegistering: DependencyRegistering { /// - 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) + func register(type: Dependency.Type, factory: @escaping FactoryWithOneArgument) } // MARK: Overloaded factory methods @@ -44,7 +44,7 @@ public extension DependencyWithArgumentRegistering { /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping FactoryWithArgument) { + func register(factory: @escaping FactoryWithOneArgument) { register(type: Dependency.self, factory: factory) } } From d5415c8ca6880b9fe7a36b042fc592e843481aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 13 Jan 2026 11:47:52 +0100 Subject: [PATCH 02/52] feat: promising --- Sources/Container/Async/AsyncContainer.swift | 81 +++++++++- Sources/Container/Sync/Container.swift | 81 +++++++++- Sources/Models/Async/AsyncRegistration.swift | 32 +++- Sources/Models/RegistrationIdentifier.swift | 26 +++- Sources/Models/Sync/Registration.swift | 30 ++++ .../Async/AsyncDependencyRegistering.swift | 78 +++++++++- ...ependencyWithArgumentAutoregistering.swift | 12 +- .../DependencyWithArgumentRegistering.swift | 6 +- ...endencyWithThreeArgumentsRegistering.swift | 50 +++++++ ...ependencyWithTwoArgumentsRegistering.swift | 50 +++++++ .../Async/AsyncDependencyResolving.swift | 69 +++++++++ ...ependencyWithThreeArgumentsResolving.swift | 49 ++++++ .../DependencyWithTwoArgumentsResolving.swift | 46 ++++++ Tests/Common/Dependencies.swift | 44 ++++++ .../Container/Async/AsyncArgumentTests.swift | 139 ++++++++++++++++++ Tests/Container/Sync/ArgumentTests.swift | 109 ++++++++++++++ 16 files changed, 882 insertions(+), 20 deletions(-) create mode 100644 Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift create mode 100644 Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift create mode 100644 Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift create mode 100644 Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index 1333503..fa0521d 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -74,6 +74,50 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin registrations[registration.identifier] = registration } + // MARK: Register dependency with two arguments + + /// Register a dependency with two arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 + public func register(type: Dependency.Type, factory: @escaping FactoryWithTwoArguments) async { + let registration = AsyncRegistration(type: type, scope: .new, factory: factory) + + registrations[registration.identifier] = registration + } + + // MARK: Register dependency with three arguments + + /// Register a dependency with three arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 + public func register(type: Dependency.Type, factory: @escaping FactoryWithThreeArguments) async { + let registration = AsyncRegistration(type: type, scope: .new, factory: factory) + + registrations[registration.identifier] = registration + } + // MARK: Resolve dependency /// Resolve a dependency that was previously registered with `register` method @@ -106,6 +150,41 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin return try await getDependency(from: registration) as Dependency } + + /// Resolve a dependency that was previously registered with `register` method + /// + /// 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 + /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method + public func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2) async throws -> Dependency { + let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + + let registration = try getRegistration(with: identifier) + + return try await getDependency(from: registration, with: (argument1, argument2)) as Dependency + } + + /// Resolve a dependency that was previously registered with `register` method + /// + /// 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 + /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - argument3: Third argument that will passed as an input parameter to the factory method that was defined with `register` method + public func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async throws -> Dependency { + let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) + + let registration = try getRegistration(with: identifier) + + return try await getDependency(from: registration, with: (argument1, argument2, argument3)) as Dependency + } } // MARK: Private methods @@ -120,7 +199,7 @@ private extension AsyncContainer { return registration } - func getDependency(from registration: AsyncRegistration, with argument: (any Sendable)? = nil) async throws -> Dependency { + func getDependency(from registration: AsyncRegistration, with argument: Any? = nil) async throws -> Dependency { switch registration.scope { case .shared: if let dependency = sharedInstances[registration.identifier] as? Dependency { diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 11657b5..1af051c 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: DependencyWithArgumentAutoregistering, DependencyAutoregistering, DependencyWithArgumentResolving, DependencyWithTwoArgumentsRegistering, DependencyWithThreeArgumentsRegistering, DependencyWithTwoArgumentsResolving, DependencyWithThreeArgumentsResolving, @unchecked Sendable { /// Shared singleton public static let shared: Container = .init() @@ -70,6 +70,50 @@ open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregis registrations[registration.identifier] = registration } + // MARK: Register dependency with two arguments + + /// Register a dependency with two arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 + open func register(type: Dependency.Type, factory: @escaping FactoryWithTwoArguments) { + let registration = Registration(type: type, scope: .new, factory: factory) + + registrations[registration.identifier] = registration + } + + // MARK: Register dependency with three arguments + + /// Register a dependency with three arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 + open func register(type: Dependency.Type, factory: @escaping FactoryWithThreeArguments) { + let registration = Registration(type: type, scope: .new, factory: factory) + + registrations[registration.identifier] = registration + } + // MARK: Resolve dependency /// Resolve a dependency that was previously registered with `register` method @@ -102,6 +146,41 @@ open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregis return try getDependency(from: registration) as Dependency } + + /// Resolve a dependency that was previously registered with `register` method + /// + /// 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 + /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method + open func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2) throws -> Dependency { + let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + + let registration = try getRegistration(with: identifier) + + return try getDependency(from: registration, with: (argument1, argument2)) as Dependency + } + + /// Resolve a dependency that was previously registered with `register` method + /// + /// 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 + /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - argument3: Third argument that will passed as an input parameter to the factory method that was defined with `register` method + open func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) throws -> Dependency { + let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) + + let registration = try getRegistration(with: identifier) + + return try getDependency(from: registration, with: (argument1, argument2, argument3)) as Dependency + } } // MARK: Private methods diff --git a/Sources/Models/Async/AsyncRegistration.swift b/Sources/Models/Async/AsyncRegistration.swift index 0022e2c..39c0f4d 100644 --- a/Sources/Models/Async/AsyncRegistration.swift +++ b/Sources/Models/Async/AsyncRegistration.swift @@ -7,7 +7,7 @@ import Foundation -typealias AsyncRegistrationFactory = @Sendable (any AsyncDependencyResolving, (any Sendable)?) async throws -> any Sendable +typealias AsyncRegistrationFactory = @Sendable (any AsyncDependencyResolving, Any?) async throws -> any Sendable /// Object that represents a registered dependency and stores a closure, i.e. a factory that returns the desired dependency struct AsyncRegistration: Sendable { @@ -36,4 +36,34 @@ struct AsyncRegistration: Sendable { return await factory(resolver, argument) } } + + /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved + init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2) async -> T) { + let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + + identifier = registrationIdentifier + self.scope = scope + asyncRegistrationFactory = { resolver, arg in + guard let arguments = arg as? (Argument1, Argument2) else { + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self))") + } + + return await factory(resolver, arguments.0, arguments.1) + } + } + + /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved + init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2, Argument3) async -> T) { + let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) + + identifier = registrationIdentifier + self.scope = scope + asyncRegistrationFactory = { resolver, arg in + guard let arguments = arg as? (Argument1, Argument2, Argument3) else { + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self), \(Argument3.self))") + } + + return await factory(resolver, arguments.0, arguments.1, arguments.2) + } + } } diff --git a/Sources/Models/RegistrationIdentifier.swift b/Sources/Models/RegistrationIdentifier.swift index 9f248e5..338767c 100644 --- a/Sources/Models/RegistrationIdentifier.swift +++ b/Sources/Models/RegistrationIdentifier.swift @@ -10,16 +10,26 @@ import Foundation /// Object that uniquely identifies a registered dependency struct RegistrationIdentifier { let typeIdentifier: ObjectIdentifier - let argumentIdentifier: ObjectIdentifier? + let argumentIdentifiers: [ObjectIdentifier] init(type: Dependency.Type, argument _: Argument.Type) { typeIdentifier = ObjectIdentifier(type) - argumentIdentifier = ObjectIdentifier(type) + argumentIdentifiers = [ObjectIdentifier(Argument.self)] + } + + init(type: Dependency.Type, argument1 _: Argument1.Type, argument2 _: Argument2.Type) { + typeIdentifier = ObjectIdentifier(type) + argumentIdentifiers = [ObjectIdentifier(Argument1.self), ObjectIdentifier(Argument2.self)] + } + + init(type: Dependency.Type, argument1 _: Argument1.Type, argument2 _: Argument2.Type, argument3 _: Argument3.Type) { + typeIdentifier = ObjectIdentifier(type) + argumentIdentifiers = [ObjectIdentifier(Argument1.self), ObjectIdentifier(Argument2.self), ObjectIdentifier(Argument3.self)] } init(type: Dependency.Type) { typeIdentifier = ObjectIdentifier(type) - argumentIdentifier = nil + argumentIdentifiers = [] } } @@ -29,9 +39,15 @@ extension RegistrationIdentifier: Hashable {} // MARK: Debug information extension RegistrationIdentifier: CustomStringConvertible { var description: String { - """ + let argumentsDescription: String + if argumentIdentifiers.isEmpty { + argumentsDescription = "nil" + } else { + argumentsDescription = argumentIdentifiers.map { $0.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..95f45b0 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -34,4 +34,34 @@ struct Registration { return factory(resolver, argument) } } + + /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithArgumentResolving, Argument1, Argument2) -> T) { + let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + + identifier = registrationIdentifier + self.scope = scope + self.factory = { resolver, arg in + guard let arguments = arg as? (Argument1, Argument2) else { + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self))") + } + + return factory(resolver, arguments.0, arguments.1) + } + } + + /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithArgumentResolving, Argument1, Argument2, Argument3) -> T) { + let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) + + identifier = registrationIdentifier + self.scope = scope + self.factory = { resolver, arg in + guard let arguments = arg as? (Argument1, Argument2, Argument3) else { + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self), \(Argument3.self))") + } + + return factory(resolver, arguments.0, arguments.1, arguments.2) + } + } } diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index 20a4f1f..763b650 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -13,8 +13,14 @@ public protocol AsyncDependencyRegistering { 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 + typealias FactoryWithOneArgument = @Sendable (any AsyncDependencyResolving, Argument) async -> Dependency + /// Factory closure that instantiates the required dependency with two variable arguments + typealias FactoryWithTwoArguments = @Sendable (any AsyncDependencyResolving, Argument1, Argument2) async -> Dependency + + /// Factory closure that instantiates the required dependency with three variable arguments + typealias FactoryWithThreeArguments = @Sendable (any AsyncDependencyResolving, Argument1, Argument2, Argument3) async -> Dependency + /// Register a dependency /// /// - Parameters: @@ -37,7 +43,39 @@ public protocol AsyncDependencyRegistering { /// - 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 FactoryWithOneArgument) async + + /// Register a dependency with two variable arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) async + + /// Register a dependency with three variable arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) async } // MARK: Overloaded factory methods @@ -88,7 +126,41 @@ public extension AsyncDependencyRegistering { /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping FactoryWithArgument) async { + func register(factory: @escaping FactoryWithOneArgument) async { + await register(type: Dependency.self, factory: factory) + } + + /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) async { + await register(type: Dependency.self, factory: factory) + } + + /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) async { await register(type: Dependency.self, factory: factory) } } diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift index 10aae96..d1cef8d 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift @@ -168,7 +168,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Argument) -> Dependency ) { - let factory: FactoryWithArgument = { _, argument in + let factory: FactoryWithOneArgument = { _, argument in initializer( argument ) @@ -202,7 +202,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Argument, Parameter) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( argument, resolver.resolve(type: Parameter.self) @@ -230,7 +230,7 @@ public extension DependencyWithArgumentAutoregistering { /// - 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 + let factory: FactoryWithOneArgument = { resolver, argument in initializer( resolver.resolve(type: Parameter.self), argument @@ -265,7 +265,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Argument, Parameter1, Parameter2) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( argument, resolver.resolve(type: Parameter1.self), @@ -298,7 +298,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Parameter1, Argument, Parameter2) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), argument, @@ -331,7 +331,7 @@ public extension DependencyWithArgumentAutoregistering { argument: Argument.Type, initializer: @escaping (Parameter1, Parameter2, Argument) -> Dependency ) { - let factory: FactoryWithArgument = { resolver, argument in + let factory: FactoryWithOneArgument = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), resolver.resolve(type: Parameter2.self), diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift index 1674f59..385b0e3 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift @@ -10,7 +10,7 @@ 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 + typealias FactoryWithOneArgument = (DependencyWithArgumentResolving, Argument) -> Dependency /// Register a dependency with a variable argument /// @@ -26,7 +26,7 @@ public protocol DependencyWithArgumentRegistering: DependencyRegistering { /// - 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) + func register(type: Dependency.Type, factory: @escaping FactoryWithOneArgument) } // MARK: Overloaded factory methods @@ -44,7 +44,7 @@ public extension DependencyWithArgumentRegistering { /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping FactoryWithArgument) { + func register(factory: @escaping FactoryWithOneArgument) { register(type: Dependency.self, factory: factory) } } diff --git a/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift new file mode 100644 index 0000000..a81651c --- /dev/null +++ b/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift @@ -0,0 +1,50 @@ +// +// DependencyWithThreeArgumentsRegistering.swift +// +// +// Created by Jan Schwarz on 26.03.2021. +// + +import Foundation + +/// A type that is able to register a dependency that needs three variable arguments in order to be resolved later +public protocol DependencyWithThreeArgumentsRegistering: DependencyRegistering { + /// Factory closure that instantiates the required dependency with three given variable arguments + typealias FactoryWithThreeArguments = (DependencyWithArgumentResolving, Argument1, Argument2, Argument3) -> Dependency + + /// Register a dependency with three variable arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) +} + +// MARK: Overloaded factory methods +public extension DependencyWithThreeArgumentsRegistering { + /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) { + register(type: Dependency.self, factory: factory) + } +} diff --git a/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift new file mode 100644 index 0000000..b258383 --- /dev/null +++ b/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift @@ -0,0 +1,50 @@ +// +// DependencyWithTwoArgumentsRegistering.swift +// +// +// Created by Jan Schwarz on 26.03.2021. +// + +import Foundation + +/// A type that is able to register a dependency that needs two variable arguments in order to be resolved later +public protocol DependencyWithTwoArgumentsRegistering: DependencyRegistering { + /// Factory closure that instantiates the required dependency with two given variable arguments + typealias FactoryWithTwoArguments = (DependencyWithArgumentResolving, Argument1, Argument2) -> Dependency + + /// Register a dependency with two variable arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) +} + +// MARK: Overloaded factory methods +public extension DependencyWithTwoArgumentsRegistering { + /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) { + register(type: Dependency.self, factory: factory) + } +} diff --git a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift index 46de4f7..dcf3543 100644 --- a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift +++ b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift @@ -25,6 +25,27 @@ public protocol AsyncDependencyResolving { /// - 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 + + /// Resolve a dependency with two 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) async throws -> T + + /// Resolve a dependency with three 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async throws -> T } public extension AsyncDependencyResolving { @@ -66,4 +87,52 @@ public extension AsyncDependencyResolving { func resolve(argument: Argument) async -> T { await resolve(type: T.self, argument: argument) } + + /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) async -> T { + try! await tryResolve(type: type, argument1: argument1, argument2: argument2) + } + + /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func resolve(argument1: Argument1, argument2: Argument2) async -> T { + await resolve(type: T.self, argument1: argument1, argument2: argument2) + } + + /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async -> T { + try! await tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) + } + + /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) async -> T { + await resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) + } } diff --git a/Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift new file mode 100644 index 0000000..c4c34d7 --- /dev/null +++ b/Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift @@ -0,0 +1,49 @@ +// +// DependencyWithThreeArgumentsResolving.swift +// +// +// Created by Jan Schwarz on 26.03.2021. +// + +import Foundation + +/// A type that is able to resolve a dependency with three given variable arguments +public protocol DependencyWithThreeArgumentsResolving: DependencyResolving { + /// Resolve a dependency with three 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) throws -> T +} + +public extension DependencyWithThreeArgumentsResolving { + /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { + try! tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) + } + + /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { + resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) + } +} diff --git a/Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift new file mode 100644 index 0000000..8bebae8 --- /dev/null +++ b/Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift @@ -0,0 +1,46 @@ +// +// DependencyWithTwoArgumentsResolving.swift +// +// +// Created by Jan Schwarz on 26.03.2021. +// + +import Foundation + +/// A type that is able to resolve a dependency with two given variable arguments +public protocol DependencyWithTwoArgumentsResolving: DependencyResolving { + /// Resolve a dependency with two 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) throws -> T +} + +public extension DependencyWithTwoArgumentsResolving { + /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) -> T { + try! tryResolve(type: type, argument1: argument1, argument2: argument2) + } + + /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func resolve(argument1: Argument1, argument2: Argument2) -> T { + resolve(type: T.self, argument1: argument1, argument2: argument2) + } +} 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..ba8832b 100644 --- a/Tests/Container/Async/AsyncArgumentTests.swift +++ b/Tests/Container/Async/AsyncArgumentTests.swift @@ -67,4 +67,143 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { 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(argument1: argument1, argument2: 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(argument1: argument1, argument2: argument2) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + } + + func testUnmatchingTwoArgumentsType() 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, argument1: argument1, argument2: argument2) + + XCTFail("Expected to throw error") + } catch { + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .dependencyNotRegistered: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error") + } + } + } + + 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(argument1: argument1, argument2: argument2, argument3: 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(argument1: argument1, argument2: argument2, argument3: 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 testUnmatchingThreeArgumentsType() 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, argument1: argument1, argument2: argument2, argument3: argument3) + + XCTFail("Expected to throw error") + } catch { + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .dependencyNotRegistered: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error") + } + } + } + + 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(argument1: argument1, argument2: 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(argument1: argument1, argument2: argument2, argument3: 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/Sync/ArgumentTests.swift b/Tests/Container/Sync/ArgumentTests.swift index 29ebca6..77dd7e1 100644 --- a/Tests/Container/Sync/ArgumentTests.swift +++ b/Tests/Container/Sync/ArgumentTests.swift @@ -55,4 +55,113 @@ final class ContainerArgumentTests: DITestCase { } } } + + 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(argument1: argument1, argument2: 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(argument1: argument1, argument2: argument2) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + } + + func testUnmatchingTwoArgumentsType() { + container.register { _, argument1, argument2 -> DependencyWithTwoArguments in + DependencyWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = 48 + let argument2 = "test" + + XCTAssertThrowsError( + try container.tryResolve(type: DependencyWithTwoArguments.self, argument1: argument1, argument2: argument2), + "Resolver didn't throw an error" + ) { error in + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .dependencyNotRegistered: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error") + } + } + } + + 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(argument1: argument1, argument2: argument2, argument3: 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(argument1: argument1, argument2: argument2, argument3: 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 testUnmatchingThreeArgumentsType() { + 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, argument1: argument1, argument2: argument2, argument3: argument3), + "Resolver didn't throw an error" + ) { error in + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .dependencyNotRegistered: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error") + } + } + } } From 33bd5db5468ac9078033e785dc7a5c2c83bf4687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 13 Jan 2026 11:52:31 +0100 Subject: [PATCH 03/52] feat: renaming --- README.md | 2 +- Sources/Container/Sync/Container.swift | 2 +- Sources/Models/Sync/Registration.swift | 8 ++++---- ... => DependencyWithOneArgumentAutoregistering.swift} | 10 +++++----- ...wift => DependencyWithOneArgumentRegistering.swift} | 8 ++++---- .../Sync/DependencyWithThreeArgumentsRegistering.swift | 2 +- .../Sync/DependencyWithTwoArgumentsRegistering.swift | 2 +- ....swift => DependencyWithOneArgumentResolving.swift} | 6 +++--- 8 files changed, 20 insertions(+), 20 deletions(-) rename Sources/Protocols/Registration/Sync/{DependencyWithArgumentAutoregistering.swift => DependencyWithOneArgumentAutoregistering.swift} (98%) rename Sources/Protocols/Registration/Sync/{DependencyWithArgumentRegistering.swift => DependencyWithOneArgumentRegistering.swift} (92%) rename Sources/Protocols/Resolution/Sync/{DependencyWithArgumentResolving.swift => DependencyWithOneArgumentResolving.swift} (92%) diff --git a/README.md b/README.md index 5dc6917..ab8925d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ 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 an argument](Sources/Protocols/Registration/DependencyWithOneArgumentRegistering.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 diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 1af051c..9c1cc31 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, DependencyWithTwoArgumentsRegistering, DependencyWithThreeArgumentsRegistering, DependencyWithTwoArgumentsResolving, DependencyWithThreeArgumentsResolving, @unchecked Sendable { +open class Container: DependencyWithOneArgumentAutoregistering, DependencyAutoregistering, DependencyWithOneArgumentResolving, DependencyWithTwoArgumentsRegistering, DependencyWithThreeArgumentsRegistering, DependencyWithTwoArgumentsResolving, DependencyWithThreeArgumentsResolving, @unchecked Sendable { /// Shared singleton public static let shared: Container = .init() diff --git a/Sources/Models/Sync/Registration.swift b/Sources/Models/Sync/Registration.swift index 95f45b0..488cc69 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -11,7 +11,7 @@ import Foundation struct Registration { let identifier: RegistrationIdentifier let scope: DependencyScope - let factory: (DependencyWithArgumentResolving, Any?) throws -> Any + let factory: (DependencyWithOneArgumentResolving, Any?) throws -> Any /// Initializer for registrations that don't need any variable argument init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyResolving) -> T) { @@ -21,7 +21,7 @@ struct Registration { } /// 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) { + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithOneArgumentResolving, Argument) -> T) { let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) identifier = registrationIdentifier @@ -36,7 +36,7 @@ struct Registration { } /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithArgumentResolving, Argument1, Argument2) -> T) { + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithOneArgumentResolving, Argument1, Argument2) -> T) { let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) identifier = registrationIdentifier @@ -51,7 +51,7 @@ struct Registration { } /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithArgumentResolving, Argument1, Argument2, Argument3) -> T) { + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithOneArgumentResolving, Argument1, Argument2, Argument3) -> T) { let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) identifier = registrationIdentifier diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift similarity index 98% rename from Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift rename to Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift index d1cef8d..05c4aaf 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift @@ -1,5 +1,5 @@ // -// DependencyWithArgumentAutoregistering.swift +// DependencyWithOneArgumentAutoregistering.swift // // // Created by Jan Schwarz on 05.08.2021. @@ -8,7 +8,7 @@ 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 { +public protocol DependencyWithOneArgumentAutoregistering: DependencyWithOneArgumentRegistering { // 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 @@ -150,7 +150,7 @@ public protocol DependencyWithArgumentAutoregistering: DependencyWithArgumentReg } // MARK: Default implementation for an initializer with a variable argument -public extension DependencyWithArgumentAutoregistering { +public extension DependencyWithOneArgumentAutoregistering { /// 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. @@ -179,7 +179,7 @@ public extension DependencyWithArgumentAutoregistering { } // MARK: Default implementation for an initializer with a variable argument and 1 parameter -public extension DependencyWithArgumentAutoregistering { +public extension DependencyWithOneArgumentAutoregistering { /// 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. @@ -242,7 +242,7 @@ public extension DependencyWithArgumentAutoregistering { } // MARK: Default implementation for an initializer with a variable argument and 2 parameters -public extension DependencyWithArgumentAutoregistering { +public extension DependencyWithOneArgumentAutoregistering { /// 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. diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentRegistering.swift similarity index 92% rename from Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift rename to Sources/Protocols/Registration/Sync/DependencyWithOneArgumentRegistering.swift index 385b0e3..3d5c897 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentRegistering.swift @@ -1,5 +1,5 @@ // -// DependencyWithArgumentRegistering.swift +// DependencyWithOneArgumentRegistering.swift // // // Created by Jan Schwarz on 26.03.2021. @@ -8,9 +8,9 @@ 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 { +public protocol DependencyWithOneArgumentRegistering: DependencyRegistering { /// Factory closure that instantiates the required dependency with the given variable argument - typealias FactoryWithOneArgument = (DependencyWithArgumentResolving, Argument) -> Dependency + typealias FactoryWithOneArgument = (DependencyWithOneArgumentResolving, Argument) -> Dependency /// Register a dependency with a variable argument /// @@ -30,7 +30,7 @@ public protocol DependencyWithArgumentRegistering: DependencyRegistering { } // MARK: Overloaded factory methods -public extension DependencyWithArgumentRegistering { +public extension DependencyWithOneArgumentRegistering { /// 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), diff --git a/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift index a81651c..609380c 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift @@ -10,7 +10,7 @@ import Foundation /// A type that is able to register a dependency that needs three variable arguments in order to be resolved later public protocol DependencyWithThreeArgumentsRegistering: DependencyRegistering { /// Factory closure that instantiates the required dependency with three given variable arguments - typealias FactoryWithThreeArguments = (DependencyWithArgumentResolving, Argument1, Argument2, Argument3) -> Dependency + typealias FactoryWithThreeArguments = (DependencyWithOneArgumentResolving, Argument1, Argument2, Argument3) -> Dependency /// Register a dependency with three variable arguments /// diff --git a/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift index b258383..e91d8af 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift @@ -10,7 +10,7 @@ import Foundation /// A type that is able to register a dependency that needs two variable arguments in order to be resolved later public protocol DependencyWithTwoArgumentsRegistering: DependencyRegistering { /// Factory closure that instantiates the required dependency with two given variable arguments - typealias FactoryWithTwoArguments = (DependencyWithArgumentResolving, Argument1, Argument2) -> Dependency + typealias FactoryWithTwoArguments = (DependencyWithOneArgumentResolving, Argument1, Argument2) -> Dependency /// Register a dependency with two variable arguments /// diff --git a/Sources/Protocols/Resolution/Sync/DependencyWithArgumentResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyWithOneArgumentResolving.swift similarity index 92% rename from Sources/Protocols/Resolution/Sync/DependencyWithArgumentResolving.swift rename to Sources/Protocols/Resolution/Sync/DependencyWithOneArgumentResolving.swift index c688a19..ca88b26 100644 --- a/Sources/Protocols/Resolution/Sync/DependencyWithArgumentResolving.swift +++ b/Sources/Protocols/Resolution/Sync/DependencyWithOneArgumentResolving.swift @@ -1,5 +1,5 @@ // -// DependencyWithArgumentResolving.swift +// DependencyWithOneArgumentResolving.swift // // // Created by Jan Schwarz on 26.03.2021. @@ -8,7 +8,7 @@ import Foundation /// A type that is able to resolve a dependency with a given variable argument -public protocol DependencyWithArgumentResolving: DependencyResolving { +public protocol DependencyWithOneArgumentResolving: 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 @@ -19,7 +19,7 @@ public protocol DependencyWithArgumentResolving: DependencyResolving { func tryResolve(type: T.Type, argument: Argument) throws -> T } -public extension DependencyWithArgumentResolving { +public extension DependencyWithOneArgumentResolving { /// 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 From 0d72519e42532ee57b29435b516546b91fa80ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 13 Jan 2026 12:01:29 +0100 Subject: [PATCH 04/52] feat: unification --- Sources/Container/Sync/Container.swift | 2 +- Sources/Models/Sync/Registration.swift | 8 +- .../Sync/DependencyRegistering.swift | 108 ++++++++++++++++++ ...ndencyWithOneArgumentAutoregistering.swift | 2 +- ...DependencyWithOneArgumentRegistering.swift | 50 -------- ...endencyWithThreeArgumentsRegistering.swift | 50 -------- ...ependencyWithTwoArgumentsRegistering.swift | 50 -------- .../Resolution/Sync/DependencyResolving.swift | 100 +++++++++++++++- .../DependencyWithOneArgumentResolving.swift | 44 ------- ...ependencyWithThreeArgumentsResolving.swift | 49 -------- .../DependencyWithTwoArgumentsResolving.swift | 46 -------- 11 files changed, 212 insertions(+), 297 deletions(-) delete mode 100644 Sources/Protocols/Registration/Sync/DependencyWithOneArgumentRegistering.swift delete mode 100644 Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift delete mode 100644 Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift delete mode 100644 Sources/Protocols/Resolution/Sync/DependencyWithOneArgumentResolving.swift delete mode 100644 Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift delete mode 100644 Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 9c1cc31..3d7a24b 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: DependencyWithOneArgumentAutoregistering, DependencyAutoregistering, DependencyWithOneArgumentResolving, DependencyWithTwoArgumentsRegistering, DependencyWithThreeArgumentsRegistering, DependencyWithTwoArgumentsResolving, DependencyWithThreeArgumentsResolving, @unchecked Sendable { +open class Container: DependencyWithOneArgumentAutoregistering, DependencyAutoregistering, DependencyResolving, DependencyRegistering, @unchecked Sendable { /// Shared singleton public static let shared: Container = .init() diff --git a/Sources/Models/Sync/Registration.swift b/Sources/Models/Sync/Registration.swift index 488cc69..4e0c245 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -11,7 +11,7 @@ import Foundation struct Registration { let identifier: RegistrationIdentifier let scope: DependencyScope - let factory: (DependencyWithOneArgumentResolving, 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) { @@ -21,7 +21,7 @@ struct Registration { } /// 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 (DependencyWithOneArgumentResolving, Argument) -> T) { + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument) -> T) { let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) identifier = registrationIdentifier @@ -36,7 +36,7 @@ struct Registration { } /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithOneArgumentResolving, Argument1, Argument2) -> T) { + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2) -> T) { let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) identifier = registrationIdentifier @@ -51,7 +51,7 @@ struct Registration { } /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithOneArgumentResolving, Argument1, Argument2, Argument3) -> T) { + init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2, Argument3) -> T) { let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) identifier = registrationIdentifier diff --git a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift index 733d4bd..1d7a766 100644 --- a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift @@ -12,6 +12,15 @@ public protocol DependencyRegistering { /// Factory closure that instantiates the required dependency typealias Factory = (DependencyResolving) -> Dependency + /// Factory closure that instantiates the required dependency with the given variable argument + typealias FactoryWithOneArgument = (DependencyResolving, Argument) -> Dependency + + /// Factory closure that instantiates the required dependency with two given variable arguments + typealias FactoryWithTwoArguments = (DependencyResolving, Argument1, Argument2) -> Dependency + + /// Factory closure that instantiates the required dependency with three given variable arguments + typealias FactoryWithThreeArguments = (DependencyResolving, Argument1, Argument2, Argument3) -> Dependency + /// Register a dependency /// /// - Parameters: @@ -19,6 +28,54 @@ 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 a variable argument + /// + /// 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: + /// - 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 FactoryWithOneArgument) + + /// Register a dependency with two variable arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) + + /// Register a dependency with three variable arguments + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) } // MARK: Overloaded factory methods @@ -55,6 +112,57 @@ public extension DependencyRegistering { func register(factory: @escaping Factory) { 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 + /// + /// 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 FactoryWithOneArgument) { + register(type: Dependency.self, factory: factory) + } + + /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) { + register(type: Dependency.self, factory: factory) + } + + /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// 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 + /// + /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) { + register(type: Dependency.self, factory: factory) + } } // MARK: Overloaded autoclosure methods diff --git a/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift index 05c4aaf..094d4b9 100644 --- a/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift @@ -8,7 +8,7 @@ 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 DependencyWithOneArgumentAutoregistering: DependencyWithOneArgumentRegistering { +public protocol DependencyWithOneArgumentAutoregistering: DependencyRegistering { // 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 diff --git a/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentRegistering.swift deleted file mode 100644 index 3d5c897..0000000 --- a/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentRegistering.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// DependencyWithOneArgumentRegistering.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 DependencyWithOneArgumentRegistering: DependencyRegistering { - /// Factory closure that instantiates the required dependency with the given variable argument - typealias FactoryWithOneArgument = (DependencyWithOneArgumentResolving, 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 FactoryWithOneArgument) -} - -// MARK: Overloaded factory methods -public extension DependencyWithOneArgumentRegistering { - /// 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 FactoryWithOneArgument) { - register(type: Dependency.self, factory: factory) - } -} diff --git a/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift deleted file mode 100644 index 609380c..0000000 --- a/Sources/Protocols/Registration/Sync/DependencyWithThreeArgumentsRegistering.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// DependencyWithThreeArgumentsRegistering.swift -// -// -// Created by Jan Schwarz on 26.03.2021. -// - -import Foundation - -/// A type that is able to register a dependency that needs three variable arguments in order to be resolved later -public protocol DependencyWithThreeArgumentsRegistering: DependencyRegistering { - /// Factory closure that instantiates the required dependency with three given variable arguments - typealias FactoryWithThreeArguments = (DependencyWithOneArgumentResolving, Argument1, Argument2, Argument3) -> Dependency - - /// Register a dependency with three variable arguments - /// - /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) -} - -// MARK: Overloaded factory methods -public extension DependencyWithThreeArgumentsRegistering { - /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type - /// - /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithThreeArguments) { - register(type: Dependency.self, factory: factory) - } -} diff --git a/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift deleted file mode 100644 index e91d8af..0000000 --- a/Sources/Protocols/Registration/Sync/DependencyWithTwoArgumentsRegistering.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// DependencyWithTwoArgumentsRegistering.swift -// -// -// Created by Jan Schwarz on 26.03.2021. -// - -import Foundation - -/// A type that is able to register a dependency that needs two variable arguments in order to be resolved later -public protocol DependencyWithTwoArgumentsRegistering: DependencyRegistering { - /// Factory closure that instantiates the required dependency with two given variable arguments - typealias FactoryWithTwoArguments = (DependencyWithOneArgumentResolving, Argument1, Argument2) -> Dependency - - /// Register a dependency with two variable arguments - /// - /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) -} - -// MARK: Overloaded factory methods -public extension DependencyWithTwoArgumentsRegistering { - /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type - /// - /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 FactoryWithTwoArguments) { - register(type: Dependency.self, factory: factory) - } -} diff --git a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift index 7d2e47e..7ff542a 100644 --- a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift +++ b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift @@ -16,6 +16,36 @@ public protocol DependencyResolving { /// - Parameters: /// - type: Type of the dependency that should be resolved func tryResolve(type: T.Type) throws -> T + + /// 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 + + /// Resolve a dependency with two 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) throws -> T + + /// Resolve a dependency with three 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) throws -> T } public extension DependencyResolving { @@ -32,10 +62,76 @@ public extension DependencyResolving { /// 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() -> T { + resolve(type: T.self) + } + + /// 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 - func resolve() -> T { - resolve(type: T.self) + /// - 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: + /// - 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) + } + + /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) -> T { + try! tryResolve(type: type, argument1: argument1, argument2: argument2) + } + + /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + func resolve(argument1: Argument1, argument2: Argument2) -> T { + resolve(type: T.self, argument1: argument1, argument2: argument2) + } + + /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { + try! tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) + } + + /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - argument1: First argument that will be passed as an input parameter to the factory method + /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - argument3: Third argument that will be passed as an input parameter to the factory method + func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { + resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) } } diff --git a/Sources/Protocols/Resolution/Sync/DependencyWithOneArgumentResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyWithOneArgumentResolving.swift deleted file mode 100644 index ca88b26..0000000 --- a/Sources/Protocols/Resolution/Sync/DependencyWithOneArgumentResolving.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// DependencyWithOneArgumentResolving.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 DependencyWithOneArgumentResolving: 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 DependencyWithOneArgumentResolving { - /// 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/Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift deleted file mode 100644 index c4c34d7..0000000 --- a/Sources/Protocols/Resolution/Sync/DependencyWithThreeArgumentsResolving.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// DependencyWithThreeArgumentsResolving.swift -// -// -// Created by Jan Schwarz on 26.03.2021. -// - -import Foundation - -/// A type that is able to resolve a dependency with three given variable arguments -public protocol DependencyWithThreeArgumentsResolving: DependencyResolving { - /// Resolve a dependency with three 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method - func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) throws -> T -} - -public extension DependencyWithThreeArgumentsResolving { - /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method - func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { - try! tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) - } - - /// Resolve a dependency with three 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method - func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { - resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) - } -} diff --git a/Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift deleted file mode 100644 index 8bebae8..0000000 --- a/Sources/Protocols/Resolution/Sync/DependencyWithTwoArgumentsResolving.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// DependencyWithTwoArgumentsResolving.swift -// -// -// Created by Jan Schwarz on 26.03.2021. -// - -import Foundation - -/// A type that is able to resolve a dependency with two given variable arguments -public protocol DependencyWithTwoArgumentsResolving: DependencyResolving { - /// Resolve a dependency with two 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 arguments of different types than expected are passed, ``ResolutionError`` is thrown - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) throws -> T -} - -public extension DependencyWithTwoArgumentsResolving { - /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) -> T { - try! tryResolve(type: type, argument1: argument1, argument2: argument2) - } - - /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - func resolve(argument1: Argument1, argument2: Argument2) -> T { - resolve(type: T.self, argument1: argument1, argument2: argument2) - } -} From 943d9515110406b8264f9d68121a842b75c1d4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 13 Jan 2026 12:08:30 +0100 Subject: [PATCH 05/52] feat: tests fix --- Tests/Container/Async/AsyncArgumentTests.swift | 2 +- Tests/Container/Sync/ArgumentTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Container/Async/AsyncArgumentTests.swift b/Tests/Container/Async/AsyncArgumentTests.swift index ba8832b..916d02e 100644 --- a/Tests/Container/Async/AsyncArgumentTests.swift +++ b/Tests/Container/Async/AsyncArgumentTests.swift @@ -49,7 +49,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } switch resolutionError { - case .unmatchingArgumentType: + case .dependencyNotRegistered: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: XCTFail("Incorrect resolution error") diff --git a/Tests/Container/Sync/ArgumentTests.swift b/Tests/Container/Sync/ArgumentTests.swift index 77dd7e1..e90f2ad 100644 --- a/Tests/Container/Sync/ArgumentTests.swift +++ b/Tests/Container/Sync/ArgumentTests.swift @@ -48,7 +48,7 @@ final class ContainerArgumentTests: DITestCase { } switch resolutionError { - case .unmatchingArgumentType: + case .dependencyNotRegistered: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: XCTFail("Incorrect resolution error") From 4d08bc17ca27f290e74be50badf922fbe5157169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 13 Jan 2026 12:14:08 +0100 Subject: [PATCH 06/52] feat: unified structure --- Sources/Container/Sync/Container.swift | 2 +- .../Sync/DependencyAutoregistering.swift | 326 ++++++++++++++++- ...ndencyWithOneArgumentAutoregistering.swift | 344 ------------------ 3 files changed, 326 insertions(+), 346 deletions(-) delete mode 100644 Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 3d7a24b..5d97c65 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: DependencyWithOneArgumentAutoregistering, DependencyAutoregistering, DependencyResolving, DependencyRegistering, @unchecked Sendable { +open class Container: DependencyAutoregistering, DependencyResolving, DependencyRegistering, @unchecked Sendable { /// Shared singleton public static let shared: Container = .init() diff --git a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift index 3146f41..0711d6e 100644 --- a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift @@ -7,7 +7,7 @@ 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 /// @@ -80,6 +80,141 @@ public protocol DependencyAutoregistering: DependencyRegistering { in scope: DependencyScope, initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4, Parameter5) -> Dependency ) + + // 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 + /// + /// 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 + ) + + /// 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 + ) + + /// 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 @@ -211,4 +346,193 @@ public extension DependencyAutoregistering { register(type: type, in: scope, factory: factory) } + + // 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 + /// + /// 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: FactoryWithOneArgument = { _, 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 + /// + /// 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: FactoryWithOneArgument = { 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: FactoryWithOneArgument = { resolver, argument in + initializer( + resolver.resolve(type: Parameter.self), + argument + ) + } + + register(type: type, factory: factory) + } + + /// 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: FactoryWithOneArgument = { 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: FactoryWithOneArgument = { 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: FactoryWithOneArgument = { 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/DependencyWithOneArgumentAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift deleted file mode 100644 index 094d4b9..0000000 --- a/Sources/Protocols/Registration/Sync/DependencyWithOneArgumentAutoregistering.swift +++ /dev/null @@ -1,344 +0,0 @@ -// -// DependencyWithOneArgumentAutoregistering.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 DependencyWithOneArgumentAutoregistering: DependencyRegistering { - // 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 DependencyWithOneArgumentAutoregistering { - /// 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: FactoryWithOneArgument = { _, argument in - initializer( - argument - ) - } - - register(type: type, factory: factory) - } -} - -// MARK: Default implementation for an initializer with a variable argument and 1 parameter -public extension DependencyWithOneArgumentAutoregistering { - /// 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: FactoryWithOneArgument = { 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: FactoryWithOneArgument = { 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 DependencyWithOneArgumentAutoregistering { - /// 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: FactoryWithOneArgument = { 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: FactoryWithOneArgument = { 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: FactoryWithOneArgument = { resolver, argument in - initializer( - resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self), - argument - ) - } - - register(type: type, factory: factory) - } -} From 7b642bee83a1123aa37627b9ea850606ff6bd940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 13 Jan 2026 12:29:54 +0100 Subject: [PATCH 07/52] feat: docs update --- Documentation/Documentation.md | 841 +++++++++++++++++++++++++++++++++ README.md | 4 +- 2 files changed, 843 insertions(+), 2 deletions(-) create mode 100644 Documentation/Documentation.md diff --git a/Documentation/Documentation.md b/Documentation/Documentation.md new file mode 100644 index 0000000..9d1771a --- /dev/null +++ b/Documentation/Documentation.md @@ -0,0 +1,841 @@ +# Dependency Injection + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Core Concepts](#core-concepts) +- [Synchronous Container](#synchronous-container) +- [Asynchronous Container](#asynchronous-container) +- [Registration](#registration) +- [Resolution](#resolution) +- [Autoregistration](#autoregistration) +- [Property Wrappers](#property-wrappers) +- [Error Handling](#error-handling) +- [Best Practices](#best-practices) +- [API Reference](#api-reference) + +## Requirements + +- iOS/iPadOS 13.0+, macOS 10.15+, watchOS 6.0+, tvOS 13.0+ +- Xcode 11+ +- Swift 5.3+ + +## Installation + +### Swift Package Manager + +Add the following to your `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/strvcom/ios-dependency-injection.git", .upToNextMajor(from: "1.0.0")) +] +``` + +Or add it through Xcode: +1. File → Add Packages... +2. Enter the repository URL: `https://github.com/strvcom/ios-dependency-injection.git` +3. Select the version rule and add to your target + +## Quick Start + +```swift +import DependencyInjection + +// Create a container +let container = Container() + +// Register a dependency +container.register { resolver in + MyService( + apiClient: resolver.resolve(type: APIClient.self) + ) +} + +// Resolve the dependency +let service: MyService = container.resolve() +``` + +## Core Concepts + +### Container + +A container is the central component that manages dependencies. It stores registrations and provides instances when requested. The library provides two container types: + +- **`Container`** - Synchronous container for traditional dependency injection +- **`AsyncContainer`** - Asynchronous container (actor-based) for Swift concurrency + +### Factory + +A factory is a closure or function that creates an instance of a dependency. Factories receive a resolver that can be used to resolve other dependencies. + +### Scope + +Dependencies can be registered with different scopes: + +- **`.new`** - A new instance is created every time the dependency is resolved +- **`.shared`** - A single instance is created on first resolution and reused for all subsequent requests (singleton) + +**Note:** Dependencies registered with variable arguments always use `.new` scope, as the behavior of shared instances with arguments is undefined. + +### Variable Arguments + +Sometimes a dependency requires parameters that can't be registered in the container (e.g., user input, configuration values). The library supports passing 1, 2, or 3 variable arguments during resolution. + +## Synchronous Container + +The `Container` class provides synchronous dependency injection for traditional Swift code. + +### Creating a Container + +```swift +// Create a new container +let container = Container() + +// Or use the shared singleton +let container = Container.shared +``` + +### Basic Registration + +```swift +// Register with explicit type and scope +container.register(type: MyService.self, in: .shared) { resolver in + MyService( + apiClient: resolver.resolve(type: APIClient.self) + ) +} + +// Register with inferred type (default scope is .shared) +container.register { resolver in + MyService( + apiClient: resolver.resolve(type: APIClient.self) + ) +} + +// Register with explicit scope +container.register(in: .new) { resolver in + MyService( + apiClient: resolver.resolve(type: APIClient.self) + ) +} +``` + +### Registration with Variable Arguments + +The library supports registration with 1, 2, or 3 variable arguments: + +```swift +// One argument +container.register { resolver, userId in + UserService(userId: userId) +} + +// Two arguments +container.register { resolver, userId, apiKey in + AuthenticatedService(userId: userId, apiKey: apiKey) +} + +// Three arguments +container.register { resolver, userId, apiKey, environment in + ConfiguredService(userId: userId, apiKey: apiKey, environment: environment) +} +``` + +### Resolution + +```swift +// Resolve without arguments +let service: MyService = container.resolve() +let service2 = container.resolve(type: MyService.self) + +// Resolve with one argument +let userService: UserService = container.resolve(argument: "user123") +let userService2 = container.resolve(type: UserService.self, argument: "user123") + +// Resolve with two arguments +let authService: AuthenticatedService = container.resolve(argument1: "user123", argument2: "key456") +let authService2 = container.resolve(type: AuthenticatedService.self, argument1: "user123", argument2: "key456") + +// Resolve with three arguments +let configService: ConfiguredService = container.resolve(argument1: "user123", argument2: "key456", argument3: "production") +let configService2 = container.resolve(type: ConfiguredService.self, argument1: "user123", argument2: "key456", argument3: "production") +``` + +### Error Handling + +```swift +// Using tryResolve (throws errors) +do { + let service = try container.tryResolve(type: MyService.self) + // Use service +} catch ResolutionError.dependencyNotRegistered(let message) { + print("Dependency not registered: \(message)") +} catch { + print("Unexpected error: \(error)") +} + +// Using resolve (crashes on error - use with caution) +let service = container.resolve(type: MyService.self) // Will crash if not registered +``` + +## Asynchronous Container + +The `AsyncContainer` is an actor-based container designed for Swift concurrency. All operations are `async` and thread-safe. + +### Creating an Async Container + +```swift +// Create a new async container +let container = AsyncContainer() + +// Or use the shared singleton +let container = AsyncContainer.shared +``` + +### Basic Registration + +```swift +// Register with explicit type and scope +await container.register(type: MyService.self, in: .shared) { resolver in + await MyService( + apiClient: await resolver.resolve(type: APIClient.self) + ) +} + +// Register with inferred type (default scope is .shared) +await container.register { resolver in + await MyService( + apiClient: await resolver.resolve(type: APIClient.self) + ) +} +``` + +### Registration with Variable Arguments + +```swift +// One argument +await container.register { resolver, userId in + await UserService(userId: userId) +} + +// Two arguments +await container.register { resolver, userId, apiKey in + await AuthenticatedService(userId: userId, apiKey: apiKey) +} + +// Three arguments +await container.register { resolver, userId, apiKey, environment in + await ConfiguredService(userId: userId, apiKey: apiKey, environment: environment) +} +``` + +### Resolution + +```swift +// Resolve without arguments +let service: MyService = await container.resolve() +let service2 = await container.resolve(type: MyService.self) + +// Resolve with one argument +let userService: UserService = await container.resolve(argument: "user123") +let userService2 = await container.resolve(type: UserService.self, argument: "user123") + +// Resolve with two arguments +let authService: AuthenticatedService = await container.resolve(argument1: "user123", argument2: "key456") +let authService2 = await container.resolve(type: AuthenticatedService.self, argument1: "user123", argument2: "key456") + +// Resolve with three arguments +let configService: ConfiguredService = await container.resolve(argument1: "user123", argument2: "key456", argument3: "production") +let configService2 = await container.resolve(type: ConfiguredService.self, argument1: "user123", argument2: "key456", argument3: "production") +``` + +### Error Handling + +```swift +// Using tryResolve (throws errors) +do { + let service = try await container.tryResolve(type: MyService.self) + // Use service +} catch ResolutionError.dependencyNotRegistered(let message) { + print("Dependency not registered: \(message)") +} catch { + print("Unexpected error: \(error)") +} +``` + +## Registration + +### Registration Methods + +The library provides multiple ways to register dependencies: + +#### 1. Basic Registration + +```swift +container.register(type: MyService.self, in: .shared) { resolver in + MyService() +} +``` + +#### 2. Registration with Inferred Type + +```swift +container.register { resolver in + MyService() // Type inferred from return type +} +``` + +#### 3. Registration with Autoclosure + +For simple dependencies without sub-dependencies: + +```swift +container.register(dependency: MyService()) +``` + +#### 4. Registration with Variable Arguments + +```swift +// One argument +container.register { resolver, argument in + MyService(parameter: argument) +} + +// Two arguments +container.register { resolver, arg1, arg2 in + MyService(param1: arg1, param2: arg2) +} + +// Three arguments +container.register { resolver, arg1, arg2, arg3 in + MyService(param1: arg1, param2: arg2, param3: arg3) +} +``` + +### Factory Closure Parameters + +Factory closures receive: +1. **Resolver** - Used to resolve other dependencies from the container +2. **Variable Arguments** (optional) - 1, 2, or 3 arguments passed during resolution + +```swift +container.register { resolver, userId in + UserService( + userId: userId, // Variable argument + apiClient: resolver.resolve(type: APIClient.self), // Resolved dependency + logger: resolver.resolve(type: Logger.self) // Resolved dependency + ) +} +``` + +## Resolution + +### Resolution Methods + +The library provides multiple ways to resolve dependencies: + +#### 1. Type Inference + +```swift +let service: MyService = container.resolve() +``` + +#### 2. Explicit Type + +```swift +let service = container.resolve(type: MyService.self) +``` + +#### 3. With Variable Arguments + +```swift +// One argument +let service: UserService = container.resolve(argument: "user123") + +// Two arguments +let service: AuthService = container.resolve(argument1: "user123", argument2: "key456") + +// Three arguments +let service: ConfigService = container.resolve(argument1: "user123", argument2: "key456", argument3: "production") +``` + +### Error Handling + +Use `tryResolve` for error handling: + +```swift +// Synchronous +do { + let service = try container.tryResolve(type: MyService.self) +} catch ResolutionError.dependencyNotRegistered(let message) { + // Handle error +} + +// Asynchronous +do { + let service = try await container.tryResolve(type: MyService.self) +} catch ResolutionError.dependencyNotRegistered(let message) { + // Handle error +} +``` + +## Autoregistration + +Autoregistration eliminates boilerplate by automatically creating factories from initializers. + +### Basic Autoregistration + +```swift +// Instead of: +container.register { resolver in + MyService( + apiClient: resolver.resolve(type: APIClient.self) + ) +} + +// You can write: +container.autoregister(initializer: MyService.init) +``` + +### Autoregistration with Multiple Parameters + +```swift +// Autoregister with 1 parameter +container.autoregister(initializer: ServiceWithOneParam.init) + +// Autoregister with 2 parameters +container.autoregister(initializer: ServiceWithTwoParams.init) + +// Autoregister with 3 parameters +container.autoregister(initializer: ServiceWithThreeParams.init) + +// Autoregister with 4 parameters +container.autoregister(initializer: ServiceWithFourParams.init) + +// Autoregister with 5 parameters +container.autoregister(initializer: ServiceWithFiveParams.init) +``` + +### Autoregistration with Variable Arguments + +```swift +// Autoregister with one variable argument +container.autoregister(argument: String.self, initializer: UserService.init) + +// Autoregister with one variable argument and one resolved parameter +container.autoregister(argument: String.self, initializer: UserServiceWithDependency.init) + +// The initializer parameter order matters: +// - Variable argument can be first: (String, APIClient) -> UserService +// - Variable argument can be last: (APIClient, String) -> UserService +// - Variable argument can be in the middle: (APIClient, String, Logger) -> UserService +``` + +### Autoregistration with Scope + +```swift +// Autoregister with explicit scope +container.autoregister(in: .new, initializer: MyService.init) +container.autoregister(in: .shared, initializer: MyService.init) +``` + +## Property Wrappers + +The library provides two property wrappers for convenient dependency injection. + +### @Injected + +Resolves the dependency immediately when the property is initialized: + +```swift +class ViewController { + @Injected var service: MyService + @Injected(from: customContainer) var customService: MyService + + func viewDidLoad() { + service.doSomething() // Already resolved + } +} +``` + +### @LazyInjected + +Resolves the dependency lazily when first accessed: + +```swift +class ViewController { + @LazyInjected var service: MyService + @LazyInjected(from: customContainer) var customService: MyService + + func viewDidLoad() { + service.doSomething() // Resolved here on first access + } +} +``` + +**Note:** Property wrappers only work with `Container`, not `AsyncContainer`. They use `Container.shared` by default, or you can specify a custom container. + +## Error Handling + +### Resolution Errors + +The library defines `ResolutionError` enum: + +```swift +public enum ResolutionError: Error { + /// No dependency with the required type is registered within the container + case dependencyNotRegistered(message: String) + + /// The dependency with the required type is registered but the factory expects a different argument type + case unmatchingArgumentType(message: String) +} +``` + +### Handling Errors + +```swift +// Synchronous +do { + let service = try container.tryResolve(type: MyService.self) +} catch ResolutionError.dependencyNotRegistered(let message) { + print("Dependency not found: \(message)") +} catch ResolutionError.unmatchingArgumentType(let message) { + print("Argument type mismatch: \(message)") +} catch { + print("Unexpected error: \(error)") +} + +// Asynchronous +do { + let service = try await container.tryResolve(type: MyService.self) +} catch ResolutionError.dependencyNotRegistered(let message) { + print("Dependency not found: \(message)") +} catch ResolutionError.unmatchingArgumentType(let message) { + print("Argument type mismatch: \(message)") +} catch { + print("Unexpected error: \(error)") +} +``` + +## Best Practices + +### 1. Container Setup + +Create and configure your container in a dedicated location: + +```swift +class AppContainer { + static let shared = Container() + + static func configure() { + shared.autoregister(initializer: APIClient.init) + shared.autoregister(initializer: UserService.init) + shared.autoregister(initializer: DataService.init) + } +} + +// In your AppDelegate or App struct +AppContainer.configure() +``` + +### 2. Protocol-Based Registration + +Register dependencies by protocol for better testability: + +```swift +protocol APIClientProtocol { + func fetchData() -> Data +} + +class APIClient: APIClientProtocol { + func fetchData() -> Data { /* ... */ } +} + +// Register by protocol +container.register(type: APIClientProtocol.self) { resolver in + APIClient() +} + +// Resolve by protocol +let client: APIClientProtocol = container.resolve() +``` + +### 3. Scope Selection + +- Use `.shared` for services, managers, and singletons +- Use `.new` for view models, controllers, and stateful objects + +```swift +// Shared service +container.register(in: .shared) { resolver in + NetworkService() +} + +// New instance for each resolution +container.register(in: .new) { resolver in + ViewModel() +} +``` + +### 4. Variable Arguments + +Use variable arguments for: +- User-specific data (user ID, session tokens) +- Configuration values determined at runtime +- Parameters that shouldn't be singletons + +```swift +// Good: User-specific service +container.register { resolver, userId in + UserProfileService(userId: userId) +} + +// Avoid: Use resolved dependencies instead +container.register { resolver, userId in + UserService( + userId: userId, + apiClient: resolver.resolve(type: APIClient.self) // Prefer this + ) +} +``` + +### 5. Async Container Usage + +Use `AsyncContainer` when: +- Working with Swift concurrency (`async`/`await`) +- Dependencies require async initialization +- Thread safety is critical + +```swift +let container = AsyncContainer.shared + +await container.register { resolver in + await DatabaseService.initialize() +} + +let service = await container.resolve(type: DatabaseService.self) +``` + +### 6. Error Handling + +Always use `tryResolve` in production code: + +```swift +// Good +do { + let service = try container.tryResolve(type: MyService.self) + // Use service +} catch { + // Handle error gracefully +} + +// Avoid (crashes on error) +let service = container.resolve(type: MyService.self) +``` + +### 7. Testing + +Create test containers with mock dependencies: + +```swift +func testMyFeature() { + let container = Container() + container.register { _ in MockAPIClient() } + container.register { resolver in + MyService(apiClient: resolver.resolve(type: APIClientProtocol.self)) + } + + let service: MyService = container.resolve() + // Test with service +} +``` + +## API Reference + +### Container + +#### Methods + +- `register(type:in:factory:)` - Register a dependency +- `register(factory:)` - Register with inferred type +- `register(type:factory:)` - Register with one argument +- `register(type:factory:)` - Register with two arguments +- `register(type:factory:)` - Register with three arguments +- `tryResolve(type:)` - Resolve dependency (throws) +- `tryResolve(type:argument:)` - Resolve with one argument (throws) +- `tryResolve(type:argument1:argument2:)` - Resolve with two arguments (throws) +- `tryResolve(type:argument1:argument2:argument3:)` - Resolve with three arguments (throws) +- `resolve(type:)` - Resolve dependency (crashes on error) +- `resolve()` - Resolve with type inference (crashes on error) +- `resolve(type:argument:)` - Resolve with one argument (crashes on error) +- `resolve(argument:)` - Resolve with one argument, type inferred (crashes on error) +- `resolve(type:argument1:argument2:)` - Resolve with two arguments (crashes on error) +- `resolve(argument1:argument2:)` - Resolve with two arguments, type inferred (crashes on error) +- `resolve(type:argument1:argument2:argument3:)` - Resolve with three arguments (crashes on error) +- `resolve(argument1:argument2:argument3:)` - Resolve with three arguments, type inferred (crashes on error) +- `autoregister(type:in:initializer:)` - Autoregister dependency +- `autoregister(type:argument:initializer:)` - Autoregister with variable argument +- `clean()` - Remove all registrations and shared instances +- `releaseSharedInstances()` - Remove cached shared instances + +### AsyncContainer + +#### Methods + +All methods are `async` and follow the same pattern as `Container`: + +- `register(type:in:factory:) async` - Register a dependency +- `register(factory:) async` - Register with inferred type +- `register(type:factory:) async` - Register with one argument +- `register(type:factory:) async` - Register with two arguments +- `register(type:factory:) async` - Register with three arguments +- `tryResolve(type:) async throws` - Resolve dependency (throws) +- `tryResolve(type:argument:) async throws` - Resolve with one argument (throws) +- `tryResolve(type:argument1:argument2:) async throws` - Resolve with two arguments (throws) +- `tryResolve(type:argument1:argument2:argument3:) async throws` - Resolve with three arguments (throws) +- `resolve(type:) async` - Resolve dependency (crashes on error) +- `resolve() async` - Resolve with type inference (crashes on error) +- `resolve(type:argument:) async` - Resolve with one argument (crashes on error) +- `resolve(argument:) async` - Resolve with one argument, type inferred (crashes on error) +- `resolve(type:argument1:argument2:) async` - Resolve with two arguments (crashes on error) +- `resolve(argument1:argument2:) async` - Resolve with two arguments, type inferred (crashes on error) +- `resolve(type:argument1:argument2:argument3:) async` - Resolve with three arguments (crashes on error) +- `resolve(argument1:argument2:argument3:) async` - Resolve with three arguments, type inferred (crashes on error) +- `clean() async` - Remove all registrations and shared instances +- `releaseSharedInstances() async` - Remove cached shared instances + +### DependencyScope + +```swift +public enum DependencyScope: Sendable { + case new // New instance on each resolution + case shared // Singleton instance +} +``` + +### ResolutionError + +```swift +public enum ResolutionError: Error { + case dependencyNotRegistered(message: String) + case unmatchingArgumentType(message: String) +} +``` + +### Property Wrappers + +#### @Injected + +```swift +@Injected var dependency: MyService +@Injected(from: container) var dependency: MyService +``` + +#### @LazyInjected + +```swift +@LazyInjected var dependency: MyService +@LazyInjected(from: container) var dependency: MyService +``` + +## Examples + +### Complete Example: Sync Container + +```swift +import DependencyInjection + +// Define dependencies +protocol APIClientProtocol { + func fetch() -> String +} + +class APIClient: APIClientProtocol { + func fetch() -> String { return "Data" } +} + +class UserService { + let apiClient: APIClientProtocol + let userId: String + + init(apiClient: APIClientProtocol, userId: String) { + self.apiClient = apiClient + self.userId = userId + } +} + +// Setup container +let container = Container() +container.autoregister(initializer: APIClient.init) +container.register { resolver, userId in + UserService( + apiClient: resolver.resolve(type: APIClientProtocol.self), + userId: userId + ) +} + +// Use dependencies +let apiClient: APIClientProtocol = container.resolve() +let userService: UserService = container.resolve(argument: "user123") +``` + +### Complete Example: Async Container + +```swift +import DependencyInjection + +actor DatabaseService { + static func initialize() async -> DatabaseService { + // Async initialization + return DatabaseService() + } +} + +class UserRepository { + let db: DatabaseService + + init(db: DatabaseService) { + self.db = db + } +} + +// Setup async container +let container = AsyncContainer.shared +await container.register { resolver in + await DatabaseService.initialize() +} +await container.register { resolver in + UserRepository(db: await resolver.resolve(type: DatabaseService.self)) +} + +// Use dependencies +let db = await container.resolve(type: DatabaseService.self) +let repo = await container.resolve(type: UserRepository.self) +``` + +### Example: Property Wrappers + +```swift +import DependencyInjection + +class ViewModel { + @Injected var apiClient: APIClientProtocol + @LazyInjected var analytics: AnalyticsService + + func loadData() { + apiClient.fetch() // Already resolved + analytics.track() // Resolved on first access + } +} +``` + +## License + +See [LICENSE](LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please read our contributing guidelines before submitting pull requests. diff --git a/README.md b/README.md index ab8925d..6b90bb8 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. See full [documentation](Documentation/Documentation.md). ## Requirements @@ -194,7 +194,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 - [ ] Container hierarchy - [ ] Thread-safety - [ ] Detect circular dependencies From 8ffdc0ed2adec2ba73a216d84c0331ad83c5c785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Wed, 14 Jan 2026 15:22:19 +0100 Subject: [PATCH 08/52] feat: fix unnecessary comments --- .../Async/AsyncDependencyRegistering.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index 763b650..4a05e2b 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -9,16 +9,13 @@ import Foundation /// A type that is able to register a dependency public protocol AsyncDependencyRegistering { - /// Factory closure that instantiates the required dependency - typealias Factory = @Sendable (any AsyncDependencyResolving) async -> Dependency - - /// Factory closure that instantiates the required dependency with the given variable argument + /// Factory closures that instantiates the required dependency + typealias Factory = @Sendable (any AsyncDependencyResolving) async -> Dependenc + typealias FactoryWithOneArgument = @Sendable (any AsyncDependencyResolving, Argument) async -> Dependency - - /// Factory closure that instantiates the required dependency with two variable arguments + typealias FactoryWithTwoArguments = @Sendable (any AsyncDependencyResolving, Argument1, Argument2) async -> Dependency - /// Factory closure that instantiates the required dependency with three variable arguments typealias FactoryWithThreeArguments = @Sendable (any AsyncDependencyResolving, Argument1, Argument2, Argument3) async -> Dependency /// Register a dependency From cbe66a4f564f593d248028f449b5dd49b6d9c503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Wed, 14 Jan 2026 16:13:59 +0100 Subject: [PATCH 09/52] feat: documentation updates --- Documentation/Documentation.md | 841 --------------------------------- README.md | 47 +- 2 files changed, 41 insertions(+), 847 deletions(-) delete mode 100644 Documentation/Documentation.md diff --git a/Documentation/Documentation.md b/Documentation/Documentation.md deleted file mode 100644 index 9d1771a..0000000 --- a/Documentation/Documentation.md +++ /dev/null @@ -1,841 +0,0 @@ -# Dependency Injection - -## Table of Contents - -- [Requirements](#requirements) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [Core Concepts](#core-concepts) -- [Synchronous Container](#synchronous-container) -- [Asynchronous Container](#asynchronous-container) -- [Registration](#registration) -- [Resolution](#resolution) -- [Autoregistration](#autoregistration) -- [Property Wrappers](#property-wrappers) -- [Error Handling](#error-handling) -- [Best Practices](#best-practices) -- [API Reference](#api-reference) - -## Requirements - -- iOS/iPadOS 13.0+, macOS 10.15+, watchOS 6.0+, tvOS 13.0+ -- Xcode 11+ -- Swift 5.3+ - -## Installation - -### Swift Package Manager - -Add the following to your `Package.swift`: - -```swift -dependencies: [ - .package(url: "https://github.com/strvcom/ios-dependency-injection.git", .upToNextMajor(from: "1.0.0")) -] -``` - -Or add it through Xcode: -1. File → Add Packages... -2. Enter the repository URL: `https://github.com/strvcom/ios-dependency-injection.git` -3. Select the version rule and add to your target - -## Quick Start - -```swift -import DependencyInjection - -// Create a container -let container = Container() - -// Register a dependency -container.register { resolver in - MyService( - apiClient: resolver.resolve(type: APIClient.self) - ) -} - -// Resolve the dependency -let service: MyService = container.resolve() -``` - -## Core Concepts - -### Container - -A container is the central component that manages dependencies. It stores registrations and provides instances when requested. The library provides two container types: - -- **`Container`** - Synchronous container for traditional dependency injection -- **`AsyncContainer`** - Asynchronous container (actor-based) for Swift concurrency - -### Factory - -A factory is a closure or function that creates an instance of a dependency. Factories receive a resolver that can be used to resolve other dependencies. - -### Scope - -Dependencies can be registered with different scopes: - -- **`.new`** - A new instance is created every time the dependency is resolved -- **`.shared`** - A single instance is created on first resolution and reused for all subsequent requests (singleton) - -**Note:** Dependencies registered with variable arguments always use `.new` scope, as the behavior of shared instances with arguments is undefined. - -### Variable Arguments - -Sometimes a dependency requires parameters that can't be registered in the container (e.g., user input, configuration values). The library supports passing 1, 2, or 3 variable arguments during resolution. - -## Synchronous Container - -The `Container` class provides synchronous dependency injection for traditional Swift code. - -### Creating a Container - -```swift -// Create a new container -let container = Container() - -// Or use the shared singleton -let container = Container.shared -``` - -### Basic Registration - -```swift -// Register with explicit type and scope -container.register(type: MyService.self, in: .shared) { resolver in - MyService( - apiClient: resolver.resolve(type: APIClient.self) - ) -} - -// Register with inferred type (default scope is .shared) -container.register { resolver in - MyService( - apiClient: resolver.resolve(type: APIClient.self) - ) -} - -// Register with explicit scope -container.register(in: .new) { resolver in - MyService( - apiClient: resolver.resolve(type: APIClient.self) - ) -} -``` - -### Registration with Variable Arguments - -The library supports registration with 1, 2, or 3 variable arguments: - -```swift -// One argument -container.register { resolver, userId in - UserService(userId: userId) -} - -// Two arguments -container.register { resolver, userId, apiKey in - AuthenticatedService(userId: userId, apiKey: apiKey) -} - -// Three arguments -container.register { resolver, userId, apiKey, environment in - ConfiguredService(userId: userId, apiKey: apiKey, environment: environment) -} -``` - -### Resolution - -```swift -// Resolve without arguments -let service: MyService = container.resolve() -let service2 = container.resolve(type: MyService.self) - -// Resolve with one argument -let userService: UserService = container.resolve(argument: "user123") -let userService2 = container.resolve(type: UserService.self, argument: "user123") - -// Resolve with two arguments -let authService: AuthenticatedService = container.resolve(argument1: "user123", argument2: "key456") -let authService2 = container.resolve(type: AuthenticatedService.self, argument1: "user123", argument2: "key456") - -// Resolve with three arguments -let configService: ConfiguredService = container.resolve(argument1: "user123", argument2: "key456", argument3: "production") -let configService2 = container.resolve(type: ConfiguredService.self, argument1: "user123", argument2: "key456", argument3: "production") -``` - -### Error Handling - -```swift -// Using tryResolve (throws errors) -do { - let service = try container.tryResolve(type: MyService.self) - // Use service -} catch ResolutionError.dependencyNotRegistered(let message) { - print("Dependency not registered: \(message)") -} catch { - print("Unexpected error: \(error)") -} - -// Using resolve (crashes on error - use with caution) -let service = container.resolve(type: MyService.self) // Will crash if not registered -``` - -## Asynchronous Container - -The `AsyncContainer` is an actor-based container designed for Swift concurrency. All operations are `async` and thread-safe. - -### Creating an Async Container - -```swift -// Create a new async container -let container = AsyncContainer() - -// Or use the shared singleton -let container = AsyncContainer.shared -``` - -### Basic Registration - -```swift -// Register with explicit type and scope -await container.register(type: MyService.self, in: .shared) { resolver in - await MyService( - apiClient: await resolver.resolve(type: APIClient.self) - ) -} - -// Register with inferred type (default scope is .shared) -await container.register { resolver in - await MyService( - apiClient: await resolver.resolve(type: APIClient.self) - ) -} -``` - -### Registration with Variable Arguments - -```swift -// One argument -await container.register { resolver, userId in - await UserService(userId: userId) -} - -// Two arguments -await container.register { resolver, userId, apiKey in - await AuthenticatedService(userId: userId, apiKey: apiKey) -} - -// Three arguments -await container.register { resolver, userId, apiKey, environment in - await ConfiguredService(userId: userId, apiKey: apiKey, environment: environment) -} -``` - -### Resolution - -```swift -// Resolve without arguments -let service: MyService = await container.resolve() -let service2 = await container.resolve(type: MyService.self) - -// Resolve with one argument -let userService: UserService = await container.resolve(argument: "user123") -let userService2 = await container.resolve(type: UserService.self, argument: "user123") - -// Resolve with two arguments -let authService: AuthenticatedService = await container.resolve(argument1: "user123", argument2: "key456") -let authService2 = await container.resolve(type: AuthenticatedService.self, argument1: "user123", argument2: "key456") - -// Resolve with three arguments -let configService: ConfiguredService = await container.resolve(argument1: "user123", argument2: "key456", argument3: "production") -let configService2 = await container.resolve(type: ConfiguredService.self, argument1: "user123", argument2: "key456", argument3: "production") -``` - -### Error Handling - -```swift -// Using tryResolve (throws errors) -do { - let service = try await container.tryResolve(type: MyService.self) - // Use service -} catch ResolutionError.dependencyNotRegistered(let message) { - print("Dependency not registered: \(message)") -} catch { - print("Unexpected error: \(error)") -} -``` - -## Registration - -### Registration Methods - -The library provides multiple ways to register dependencies: - -#### 1. Basic Registration - -```swift -container.register(type: MyService.self, in: .shared) { resolver in - MyService() -} -``` - -#### 2. Registration with Inferred Type - -```swift -container.register { resolver in - MyService() // Type inferred from return type -} -``` - -#### 3. Registration with Autoclosure - -For simple dependencies without sub-dependencies: - -```swift -container.register(dependency: MyService()) -``` - -#### 4. Registration with Variable Arguments - -```swift -// One argument -container.register { resolver, argument in - MyService(parameter: argument) -} - -// Two arguments -container.register { resolver, arg1, arg2 in - MyService(param1: arg1, param2: arg2) -} - -// Three arguments -container.register { resolver, arg1, arg2, arg3 in - MyService(param1: arg1, param2: arg2, param3: arg3) -} -``` - -### Factory Closure Parameters - -Factory closures receive: -1. **Resolver** - Used to resolve other dependencies from the container -2. **Variable Arguments** (optional) - 1, 2, or 3 arguments passed during resolution - -```swift -container.register { resolver, userId in - UserService( - userId: userId, // Variable argument - apiClient: resolver.resolve(type: APIClient.self), // Resolved dependency - logger: resolver.resolve(type: Logger.self) // Resolved dependency - ) -} -``` - -## Resolution - -### Resolution Methods - -The library provides multiple ways to resolve dependencies: - -#### 1. Type Inference - -```swift -let service: MyService = container.resolve() -``` - -#### 2. Explicit Type - -```swift -let service = container.resolve(type: MyService.self) -``` - -#### 3. With Variable Arguments - -```swift -// One argument -let service: UserService = container.resolve(argument: "user123") - -// Two arguments -let service: AuthService = container.resolve(argument1: "user123", argument2: "key456") - -// Three arguments -let service: ConfigService = container.resolve(argument1: "user123", argument2: "key456", argument3: "production") -``` - -### Error Handling - -Use `tryResolve` for error handling: - -```swift -// Synchronous -do { - let service = try container.tryResolve(type: MyService.self) -} catch ResolutionError.dependencyNotRegistered(let message) { - // Handle error -} - -// Asynchronous -do { - let service = try await container.tryResolve(type: MyService.self) -} catch ResolutionError.dependencyNotRegistered(let message) { - // Handle error -} -``` - -## Autoregistration - -Autoregistration eliminates boilerplate by automatically creating factories from initializers. - -### Basic Autoregistration - -```swift -// Instead of: -container.register { resolver in - MyService( - apiClient: resolver.resolve(type: APIClient.self) - ) -} - -// You can write: -container.autoregister(initializer: MyService.init) -``` - -### Autoregistration with Multiple Parameters - -```swift -// Autoregister with 1 parameter -container.autoregister(initializer: ServiceWithOneParam.init) - -// Autoregister with 2 parameters -container.autoregister(initializer: ServiceWithTwoParams.init) - -// Autoregister with 3 parameters -container.autoregister(initializer: ServiceWithThreeParams.init) - -// Autoregister with 4 parameters -container.autoregister(initializer: ServiceWithFourParams.init) - -// Autoregister with 5 parameters -container.autoregister(initializer: ServiceWithFiveParams.init) -``` - -### Autoregistration with Variable Arguments - -```swift -// Autoregister with one variable argument -container.autoregister(argument: String.self, initializer: UserService.init) - -// Autoregister with one variable argument and one resolved parameter -container.autoregister(argument: String.self, initializer: UserServiceWithDependency.init) - -// The initializer parameter order matters: -// - Variable argument can be first: (String, APIClient) -> UserService -// - Variable argument can be last: (APIClient, String) -> UserService -// - Variable argument can be in the middle: (APIClient, String, Logger) -> UserService -``` - -### Autoregistration with Scope - -```swift -// Autoregister with explicit scope -container.autoregister(in: .new, initializer: MyService.init) -container.autoregister(in: .shared, initializer: MyService.init) -``` - -## Property Wrappers - -The library provides two property wrappers for convenient dependency injection. - -### @Injected - -Resolves the dependency immediately when the property is initialized: - -```swift -class ViewController { - @Injected var service: MyService - @Injected(from: customContainer) var customService: MyService - - func viewDidLoad() { - service.doSomething() // Already resolved - } -} -``` - -### @LazyInjected - -Resolves the dependency lazily when first accessed: - -```swift -class ViewController { - @LazyInjected var service: MyService - @LazyInjected(from: customContainer) var customService: MyService - - func viewDidLoad() { - service.doSomething() // Resolved here on first access - } -} -``` - -**Note:** Property wrappers only work with `Container`, not `AsyncContainer`. They use `Container.shared` by default, or you can specify a custom container. - -## Error Handling - -### Resolution Errors - -The library defines `ResolutionError` enum: - -```swift -public enum ResolutionError: Error { - /// No dependency with the required type is registered within the container - case dependencyNotRegistered(message: String) - - /// The dependency with the required type is registered but the factory expects a different argument type - case unmatchingArgumentType(message: String) -} -``` - -### Handling Errors - -```swift -// Synchronous -do { - let service = try container.tryResolve(type: MyService.self) -} catch ResolutionError.dependencyNotRegistered(let message) { - print("Dependency not found: \(message)") -} catch ResolutionError.unmatchingArgumentType(let message) { - print("Argument type mismatch: \(message)") -} catch { - print("Unexpected error: \(error)") -} - -// Asynchronous -do { - let service = try await container.tryResolve(type: MyService.self) -} catch ResolutionError.dependencyNotRegistered(let message) { - print("Dependency not found: \(message)") -} catch ResolutionError.unmatchingArgumentType(let message) { - print("Argument type mismatch: \(message)") -} catch { - print("Unexpected error: \(error)") -} -``` - -## Best Practices - -### 1. Container Setup - -Create and configure your container in a dedicated location: - -```swift -class AppContainer { - static let shared = Container() - - static func configure() { - shared.autoregister(initializer: APIClient.init) - shared.autoregister(initializer: UserService.init) - shared.autoregister(initializer: DataService.init) - } -} - -// In your AppDelegate or App struct -AppContainer.configure() -``` - -### 2. Protocol-Based Registration - -Register dependencies by protocol for better testability: - -```swift -protocol APIClientProtocol { - func fetchData() -> Data -} - -class APIClient: APIClientProtocol { - func fetchData() -> Data { /* ... */ } -} - -// Register by protocol -container.register(type: APIClientProtocol.self) { resolver in - APIClient() -} - -// Resolve by protocol -let client: APIClientProtocol = container.resolve() -``` - -### 3. Scope Selection - -- Use `.shared` for services, managers, and singletons -- Use `.new` for view models, controllers, and stateful objects - -```swift -// Shared service -container.register(in: .shared) { resolver in - NetworkService() -} - -// New instance for each resolution -container.register(in: .new) { resolver in - ViewModel() -} -``` - -### 4. Variable Arguments - -Use variable arguments for: -- User-specific data (user ID, session tokens) -- Configuration values determined at runtime -- Parameters that shouldn't be singletons - -```swift -// Good: User-specific service -container.register { resolver, userId in - UserProfileService(userId: userId) -} - -// Avoid: Use resolved dependencies instead -container.register { resolver, userId in - UserService( - userId: userId, - apiClient: resolver.resolve(type: APIClient.self) // Prefer this - ) -} -``` - -### 5. Async Container Usage - -Use `AsyncContainer` when: -- Working with Swift concurrency (`async`/`await`) -- Dependencies require async initialization -- Thread safety is critical - -```swift -let container = AsyncContainer.shared - -await container.register { resolver in - await DatabaseService.initialize() -} - -let service = await container.resolve(type: DatabaseService.self) -``` - -### 6. Error Handling - -Always use `tryResolve` in production code: - -```swift -// Good -do { - let service = try container.tryResolve(type: MyService.self) - // Use service -} catch { - // Handle error gracefully -} - -// Avoid (crashes on error) -let service = container.resolve(type: MyService.self) -``` - -### 7. Testing - -Create test containers with mock dependencies: - -```swift -func testMyFeature() { - let container = Container() - container.register { _ in MockAPIClient() } - container.register { resolver in - MyService(apiClient: resolver.resolve(type: APIClientProtocol.self)) - } - - let service: MyService = container.resolve() - // Test with service -} -``` - -## API Reference - -### Container - -#### Methods - -- `register(type:in:factory:)` - Register a dependency -- `register(factory:)` - Register with inferred type -- `register(type:factory:)` - Register with one argument -- `register(type:factory:)` - Register with two arguments -- `register(type:factory:)` - Register with three arguments -- `tryResolve(type:)` - Resolve dependency (throws) -- `tryResolve(type:argument:)` - Resolve with one argument (throws) -- `tryResolve(type:argument1:argument2:)` - Resolve with two arguments (throws) -- `tryResolve(type:argument1:argument2:argument3:)` - Resolve with three arguments (throws) -- `resolve(type:)` - Resolve dependency (crashes on error) -- `resolve()` - Resolve with type inference (crashes on error) -- `resolve(type:argument:)` - Resolve with one argument (crashes on error) -- `resolve(argument:)` - Resolve with one argument, type inferred (crashes on error) -- `resolve(type:argument1:argument2:)` - Resolve with two arguments (crashes on error) -- `resolve(argument1:argument2:)` - Resolve with two arguments, type inferred (crashes on error) -- `resolve(type:argument1:argument2:argument3:)` - Resolve with three arguments (crashes on error) -- `resolve(argument1:argument2:argument3:)` - Resolve with three arguments, type inferred (crashes on error) -- `autoregister(type:in:initializer:)` - Autoregister dependency -- `autoregister(type:argument:initializer:)` - Autoregister with variable argument -- `clean()` - Remove all registrations and shared instances -- `releaseSharedInstances()` - Remove cached shared instances - -### AsyncContainer - -#### Methods - -All methods are `async` and follow the same pattern as `Container`: - -- `register(type:in:factory:) async` - Register a dependency -- `register(factory:) async` - Register with inferred type -- `register(type:factory:) async` - Register with one argument -- `register(type:factory:) async` - Register with two arguments -- `register(type:factory:) async` - Register with three arguments -- `tryResolve(type:) async throws` - Resolve dependency (throws) -- `tryResolve(type:argument:) async throws` - Resolve with one argument (throws) -- `tryResolve(type:argument1:argument2:) async throws` - Resolve with two arguments (throws) -- `tryResolve(type:argument1:argument2:argument3:) async throws` - Resolve with three arguments (throws) -- `resolve(type:) async` - Resolve dependency (crashes on error) -- `resolve() async` - Resolve with type inference (crashes on error) -- `resolve(type:argument:) async` - Resolve with one argument (crashes on error) -- `resolve(argument:) async` - Resolve with one argument, type inferred (crashes on error) -- `resolve(type:argument1:argument2:) async` - Resolve with two arguments (crashes on error) -- `resolve(argument1:argument2:) async` - Resolve with two arguments, type inferred (crashes on error) -- `resolve(type:argument1:argument2:argument3:) async` - Resolve with three arguments (crashes on error) -- `resolve(argument1:argument2:argument3:) async` - Resolve with three arguments, type inferred (crashes on error) -- `clean() async` - Remove all registrations and shared instances -- `releaseSharedInstances() async` - Remove cached shared instances - -### DependencyScope - -```swift -public enum DependencyScope: Sendable { - case new // New instance on each resolution - case shared // Singleton instance -} -``` - -### ResolutionError - -```swift -public enum ResolutionError: Error { - case dependencyNotRegistered(message: String) - case unmatchingArgumentType(message: String) -} -``` - -### Property Wrappers - -#### @Injected - -```swift -@Injected var dependency: MyService -@Injected(from: container) var dependency: MyService -``` - -#### @LazyInjected - -```swift -@LazyInjected var dependency: MyService -@LazyInjected(from: container) var dependency: MyService -``` - -## Examples - -### Complete Example: Sync Container - -```swift -import DependencyInjection - -// Define dependencies -protocol APIClientProtocol { - func fetch() -> String -} - -class APIClient: APIClientProtocol { - func fetch() -> String { return "Data" } -} - -class UserService { - let apiClient: APIClientProtocol - let userId: String - - init(apiClient: APIClientProtocol, userId: String) { - self.apiClient = apiClient - self.userId = userId - } -} - -// Setup container -let container = Container() -container.autoregister(initializer: APIClient.init) -container.register { resolver, userId in - UserService( - apiClient: resolver.resolve(type: APIClientProtocol.self), - userId: userId - ) -} - -// Use dependencies -let apiClient: APIClientProtocol = container.resolve() -let userService: UserService = container.resolve(argument: "user123") -``` - -### Complete Example: Async Container - -```swift -import DependencyInjection - -actor DatabaseService { - static func initialize() async -> DatabaseService { - // Async initialization - return DatabaseService() - } -} - -class UserRepository { - let db: DatabaseService - - init(db: DatabaseService) { - self.db = db - } -} - -// Setup async container -let container = AsyncContainer.shared -await container.register { resolver in - await DatabaseService.initialize() -} -await container.register { resolver in - UserRepository(db: await resolver.resolve(type: DatabaseService.self)) -} - -// Use dependencies -let db = await container.resolve(type: DatabaseService.self) -let repo = await container.resolve(type: UserRepository.self) -``` - -### Example: Property Wrappers - -```swift -import DependencyInjection - -class ViewModel { - @Injected var apiClient: APIClientProtocol - @LazyInjected var analytics: AnalyticsService - - func loadData() { - apiClient.fetch() // Already resolved - analytics.track() // Resolved on first access - } -} -``` - -## License - -See [LICENSE](LICENSE) file for details. - -## Contributing - -Contributions are welcome! Please read our contributing guidelines before submitting pull requests. diff --git a/README.md b/README.md index 6b90bb8..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. See full [documentation](Documentation/Documentation.md). +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/DependencyWithOneArgumentRegistering.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 @@ -195,6 +230,6 @@ In the example above the dependencies aren't resolved immediately when an instan - [x] Autoregister - [x] SPM package - [x] Register an instance with multiple arguments +- [x] AsyncContainer for Swift concurrency (thread-safe) - [ ] Container hierarchy -- [ ] Thread-safety - [ ] Detect circular dependencies From 0e694dba8392d22469ea40bb1af67aa61cdea398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Wed, 14 Jan 2026 17:16:23 +0100 Subject: [PATCH 10/52] feat: typo fix --- .../Registration/Async/AsyncDependencyRegistering.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index 4a05e2b..096f1b5 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -10,7 +10,7 @@ import Foundation /// A type that is able to register a dependency public protocol AsyncDependencyRegistering { /// Factory closures that instantiates the required dependency - typealias Factory = @Sendable (any AsyncDependencyResolving) async -> Dependenc + typealias Factory = @Sendable (any AsyncDependencyResolving) async -> Dependency typealias FactoryWithOneArgument = @Sendable (any AsyncDependencyResolving, Argument) async -> Dependency From a282b3e83f93ec0f0d4d3e98e0a0d4c349fdc003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 15 Jan 2026 11:27:51 +0100 Subject: [PATCH 11/52] feat: sendable --- Sources/Models/RegistrationIdentifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Models/RegistrationIdentifier.swift b/Sources/Models/RegistrationIdentifier.swift index 338767c..f3e39f2 100644 --- a/Sources/Models/RegistrationIdentifier.swift +++ b/Sources/Models/RegistrationIdentifier.swift @@ -8,7 +8,7 @@ import Foundation /// Object that uniquely identifies a registered dependency -struct RegistrationIdentifier { +struct RegistrationIdentifier: Sendable { let typeIdentifier: ObjectIdentifier let argumentIdentifiers: [ObjectIdentifier] From 413d068d449219846b91986eb40b02da251656b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 15 Jan 2026 11:56:38 +0100 Subject: [PATCH 12/52] feat: unmatchingArgumentType tests --- Sources/Container/Async/AsyncContainer.swift | 15 +++++-- Sources/Container/Sync/Container.swift | 15 +++++-- .../Container/Async/AsyncArgumentTests.swift | 44 +++++++++++++++---- Tests/Container/Sync/ArgumentTests.swift | 43 ++++++++++++++---- 4 files changed, 91 insertions(+), 26 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index fa0521d..d23de47 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -190,13 +190,20 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin // 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? = nil) async throws -> Dependency { diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 5d97c65..7ca00fa 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -186,13 +186,20 @@ open class Container: DependencyAutoregistering, DependencyResolving, 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/Tests/Container/Async/AsyncArgumentTests.swift b/Tests/Container/Async/AsyncArgumentTests.swift index 916d02e..a3f0c70 100644 --- a/Tests/Container/Async/AsyncArgumentTests.swift +++ b/Tests/Container/Async/AsyncArgumentTests.swift @@ -31,7 +31,33 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } - func testUnmatchingArgumentType() async { + func testUnmatchingArgumentType_ZeroArguments() async { + await container.register { _ -> SimpleDependency in + SimpleDependency() + } + + let argument = 48 + + do { + _ = try await container.tryResolve(type: SimpleDependency.self, argument: 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) } @@ -49,10 +75,10 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } switch resolutionError { - case .dependencyNotRegistered: + case .unmatchingArgumentType: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: - XCTFail("Incorrect resolution error") + XCTFail("Incorrect resolution error: \(resolutionError)") } } } @@ -94,7 +120,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") } - func testUnmatchingTwoArgumentsType() async { + func testUnmatchingArgumentType_TwoArguments() async { await container.register { _, argument1, argument2 -> DependencyWithTwoArguments in DependencyWithTwoArguments(argument1: argument1, argument2: argument2) } @@ -113,10 +139,10 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } switch resolutionError { - case .dependencyNotRegistered: + case .unmatchingArgumentType: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: - XCTFail("Incorrect resolution error") + XCTFail("Incorrect resolution error: \(resolutionError)") } } } @@ -151,7 +177,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { XCTAssertEqual(argument3, resolvedDependency.argument3, "Container returned dependency with different third argument") } - func testUnmatchingThreeArgumentsType() async { + func testUnmatchingArgumentType_ThreeArguments() async { await container.register { _, argument1, argument2, argument3 -> DependencyWithThreeArguments in DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) } @@ -171,10 +197,10 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } switch resolutionError { - case .dependencyNotRegistered: + 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/ArgumentTests.swift b/Tests/Container/Sync/ArgumentTests.swift index e90f2ad..c61aae5 100644 --- a/Tests/Container/Sync/ArgumentTests.swift +++ b/Tests/Container/Sync/ArgumentTests.swift @@ -31,7 +31,32 @@ final class ContainerArgumentTests: DITestCase { XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } - func testUnmatchingArgumentType() { + func testUnmatchingArgumentType_ZeroArguments() { + container.register { _ -> SimpleDependency in + SimpleDependency() + } + + let argument = 48 + + XCTAssertThrowsError( + try container.tryResolve(type: SimpleDependency.self, argument: 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) } @@ -48,10 +73,10 @@ final class ContainerArgumentTests: DITestCase { } switch resolutionError { - case .dependencyNotRegistered: + case .unmatchingArgumentType: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: - XCTFail("Incorrect resolution error") + XCTFail("Incorrect resolution error: \(resolutionError)") } } } @@ -82,7 +107,7 @@ final class ContainerArgumentTests: DITestCase { XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") } - func testUnmatchingTwoArgumentsType() { + func testUnmatchingArgumentType_TwoArguments() { container.register { _, argument1, argument2 -> DependencyWithTwoArguments in DependencyWithTwoArguments(argument1: argument1, argument2: argument2) } @@ -100,10 +125,10 @@ final class ContainerArgumentTests: DITestCase { } switch resolutionError { - case .dependencyNotRegistered: + case .unmatchingArgumentType: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: - XCTFail("Incorrect resolution error") + XCTFail("Incorrect resolution error: \(resolutionError)") } } } @@ -138,7 +163,7 @@ final class ContainerArgumentTests: DITestCase { XCTAssertEqual(argument3, resolvedDependency.argument3, "Container returned dependency with different third argument") } - func testUnmatchingThreeArgumentsType() { + func testUnmatchingArgumentType_ThreeArguments() { container.register { _, argument1, argument2, argument3 -> DependencyWithThreeArguments in DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) } @@ -157,10 +182,10 @@ final class ContainerArgumentTests: DITestCase { } switch resolutionError { - case .dependencyNotRegistered: + case .unmatchingArgumentType: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: - XCTFail("Incorrect resolution error") + XCTFail("Incorrect resolution error: \(resolutionError)") } } } From ba50194e17389fb6db1480c6188a4b58f49a52ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Fri, 16 Jan 2026 12:14:03 +0100 Subject: [PATCH 13/52] feat: fix CI runner --- .github/workflows/integrations.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 2627cc6..f6ca73e 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -1,4 +1,4 @@ -### Integrations workflow ### + ### Integrations workflow ### # # Runs Danger checks, Swiftlint and tests. # @@ -18,7 +18,7 @@ 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: From 94d205e4a90a0c68eeed58e156013c9de6a7ee8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 19 Jan 2026 11:06:06 +0100 Subject: [PATCH 14/52] feat: bundler fix --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 36cee07..e5d63d4 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ source 'https://rubygems.org' gem 'bundler', '~> 2.6.3' gem 'danger', '~> 8.2.0' -gem 'fastlane', '~> 2.226.0' +gem 'fastlane', '>= 2.217.0', '< 2.219.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 index 1e1ec5a..4b2b8ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -257,7 +257,7 @@ PLATFORMS DEPENDENCIES bundler (~> 2.6.3) danger (~> 8.2.0) - fastlane (~> 2.217.0) + fastlane (>= 2.217.0, < 2.219.0) BUNDLED WITH 2.6.3 From 0a65ac603c4dd36cdec16d14e2a009b63615509e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 19 Jan 2026 11:12:05 +0100 Subject: [PATCH 15/52] feat: changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b545202..dd90dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ __Sections__ - `Removed` for deprecated features removed in this release. - `Fixed` for any bug fixes. +## [1.0.5] + +### Fixed + +- Support for multiple arguments. Code clean up. + +## [1.0.4] + +### Fixed + +- Async dependency injection fixes + ## [1.0.3] ### Fixed From 96f36a563b186889f05a9ee1a176a9b8a2038de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 20 Jan 2026 11:30:22 +0100 Subject: [PATCH 16/52] feat: removed discussions from comments --- Sources/Container/Async/AsyncContainer.swift | 29 ++--- Sources/Container/Sync/Container.swift | 27 +---- .../Async/AsyncDependencyRegistering.swift | 52 ++------- .../Sync/DependencyAutoregistering.swift | 106 +++++------------- .../Sync/DependencyRegistering.swift | 54 ++------- 5 files changed, 64 insertions(+), 204 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index d23de47..d284c19 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -57,13 +57,8 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// Register a dependency with an argument /// /// 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. + /// therefore, it needs 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 @@ -79,14 +74,9 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// Register a dependency with two arguments /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. - /// + /// 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 @@ -101,13 +91,8 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// Register a dependency with three arguments /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 7ca00fa..22a6369 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -53,13 +53,8 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// Register a dependency with an argument /// /// 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. + /// therefore, it needs 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 @@ -75,13 +70,8 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// Register a dependency with two arguments /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 @@ -97,13 +87,8 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// Register a dependency with three arguments /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index 096f1b5..7e240bb 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -29,13 +29,8 @@ public protocol AsyncDependencyRegistering { /// 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. + /// therefore, it needs 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 @@ -45,13 +40,8 @@ public protocol AsyncDependencyRegistering { /// Register a dependency with two variable arguments /// /// 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 - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. + /// 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 as the behaviour for resolving shared instances with arguments is undefined. - /// Should the arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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 @@ -61,13 +51,8 @@ public protocol AsyncDependencyRegistering { /// Register a dependency with three variable arguments /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 @@ -113,13 +98,8 @@ public extension AsyncDependencyRegistering { /// 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. + /// therefore, it needs 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 @@ -130,13 +110,8 @@ public extension AsyncDependencyRegistering { /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 @@ -147,13 +122,8 @@ public extension AsyncDependencyRegistering { /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 diff --git a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift index 0711d6e..ca32ff7 100644 --- a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift @@ -83,7 +83,8 @@ public protocol DependencyAutoregistering: DependencyRegistering { // 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 + /// 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. @@ -101,19 +102,14 @@ public protocol DependencyAutoregistering: DependencyRegistering { initializer: @escaping (Argument) -> Dependency ) - /// 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 + /// 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 /// - /// 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 @@ -124,19 +120,15 @@ public protocol DependencyAutoregistering: DependencyRegistering { 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 + /// 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 /// - /// 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 @@ -147,19 +139,14 @@ public protocol DependencyAutoregistering: DependencyRegistering { initializer: @escaping (Parameter, Argument) -> Dependency ) - /// 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 + /// 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 /// - /// 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 @@ -170,19 +157,15 @@ public protocol DependencyAutoregistering: DependencyRegistering { 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 + /// 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 /// - /// 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 @@ -193,19 +176,15 @@ public protocol DependencyAutoregistering: DependencyRegistering { 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 + /// 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 /// - /// 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 @@ -349,13 +328,8 @@ public extension DependencyAutoregistering { // 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 - /// - /// 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. + /// 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 @@ -373,19 +347,15 @@ public extension DependencyAutoregistering { 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 + /// 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 /// - /// 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 @@ -405,19 +375,14 @@ public extension DependencyAutoregistering { 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 + /// 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 /// - /// 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 @@ -437,19 +402,14 @@ public extension DependencyAutoregistering { register(type: type, factory: factory) } - /// 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 + /// 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 /// - /// 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 @@ -470,19 +430,14 @@ public extension DependencyAutoregistering { 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 + /// 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 /// - /// 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 @@ -503,19 +458,14 @@ public extension DependencyAutoregistering { 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 + /// 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 /// - /// 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 diff --git a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift index 1d7a766..4c98a62 100644 --- a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift @@ -32,13 +32,8 @@ public protocol DependencyRegistering { /// Register a dependency with a variable argument /// /// 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. + /// therefore, it needs 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 @@ -48,13 +43,8 @@ public protocol DependencyRegistering { /// Register a dependency with two variable arguments /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 @@ -64,13 +54,8 @@ public protocol DependencyRegistering { /// Register a dependency with three variable arguments /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 @@ -116,13 +101,8 @@ public extension DependencyRegistering { /// 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. + /// therefore, it needs 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 @@ -133,13 +113,8 @@ public extension DependencyRegistering { /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 @@ -150,13 +125,8 @@ public extension DependencyRegistering { /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type /// /// 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 - /// - /// 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 arguments conform to ``Equatable`` to compare the arguments to tell whether a shared instance with given arguments 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. + /// 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 From de1f2978809fcde8499856ae0d2f61986fd21527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 20 Jan 2026 11:38:26 +0100 Subject: [PATCH 17/52] feat: reduced comments --- Sources/Container/Async/AsyncContainer.swift | 7 ++----- Sources/Container/Sync/Container.swift | 7 ++----- .../Async/AsyncDependencyResolving.swift | 21 ++++++------------- .../Resolution/Sync/DependencyResolving.swift | 21 ++++++------------- 4 files changed, 16 insertions(+), 40 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index d284c19..24f1131 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -143,8 +143,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method - /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - arguments: Aarguments that will passed as an input parameter to the factory method that was defined with `register` method public func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2) async throws -> Dependency { let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) @@ -160,9 +159,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method - /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method - /// - argument3: Third argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - arguments: Aarguments that will passed as an input parameter to the factory method that was defined with `register` method public func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async throws -> Dependency { let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 22a6369..6d46030 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -139,8 +139,7 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method - /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - arguments: Arguments that will passed as an input parameter to the factory method that was defined with `register` method open func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2) throws -> Dependency { let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) @@ -156,9 +155,7 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will passed as an input parameter to the factory method that was defined with `register` method - /// - argument2: Second argument that will passed as an input parameter to the factory method that was defined with `register` method - /// - argument3: Third argument that will passed as an input parameter to the factory method that was defined with `register` method + /// - arguments: Arguments that will passed as an input parameter to the factory method that was defined with `register` method open func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) throws -> Dependency { let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) diff --git a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift index dcf3543..f5f5eaa 100644 --- a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift +++ b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift @@ -32,8 +32,7 @@ public protocol AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) async throws -> T /// Resolve a dependency with three variable arguments that was previously registered within the container @@ -42,9 +41,7 @@ public protocol AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async throws -> T } @@ -94,8 +91,7 @@ public extension AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) async -> T { try! await tryResolve(type: type, argument1: argument1, argument2: argument2) } @@ -105,8 +101,7 @@ public extension AsyncDependencyResolving { /// 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: - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(argument1: Argument1, argument2: Argument2) async -> T { await resolve(type: T.self, argument1: argument1, argument2: argument2) } @@ -117,9 +112,7 @@ public extension AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async -> T { try! await tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) } @@ -129,9 +122,7 @@ public extension AsyncDependencyResolving { /// 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: - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) async -> T { await resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) } diff --git a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift index 7ff542a..3aaa88d 100644 --- a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift +++ b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift @@ -32,8 +32,7 @@ public protocol DependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) throws -> T /// Resolve a dependency with three variable arguments that was previously registered within the container @@ -42,9 +41,7 @@ public protocol DependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) throws -> T } @@ -93,8 +90,7 @@ public extension DependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) -> T { try! tryResolve(type: type, argument1: argument1, argument2: argument2) } @@ -104,8 +100,7 @@ public extension DependencyResolving { /// 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: - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(argument1: Argument1, argument2: Argument2) -> T { resolve(type: T.self, argument1: argument1, argument2: argument2) } @@ -116,9 +111,7 @@ public extension DependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { try! tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) } @@ -128,9 +121,7 @@ public extension DependencyResolving { /// 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: - /// - argument1: First argument that will be passed as an input parameter to the factory method - /// - argument2: Second argument that will be passed as an input parameter to the factory method - /// - argument3: Third argument that will be passed as an input parameter to the factory method + /// - arguments: Arguments that will be passed as an input parameters to the factory method func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) } From 0ab4c2eb16d9810568bde33e58c434935a2261de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Feb 2026 14:44:35 +0100 Subject: [PATCH 18/52] feat: remove fastlane --- .github/workflows/integrations.yml | 4 +- Gemfile | 4 - Gemfile.lock | 199 ++--------------------------- fastlane/Fastfile | 9 -- fastlane/README.md | 28 ---- 5 files changed, 12 insertions(+), 232 deletions(-) delete mode 100644 fastlane/Fastfile delete mode 100644 fastlane/README.md diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index f6ca73e..1103e32 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -45,6 +45,4 @@ jobs: # 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/Gemfile b/Gemfile index e5d63d4..8ce1e30 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,3 @@ source 'https://rubygems.org' gem 'bundler', '~> 2.6.3' gem 'danger', '~> 8.2.0' -gem 'fastlane', '>= 2.217.0', '< 2.219.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 index 4b2b8ea..6beb88a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,42 +1,14 @@ 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) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.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) @@ -52,13 +24,6 @@ GEM 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) @@ -71,184 +36,43 @@ GEM 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-em_synchrony (1.0.1) faraday-excon (1.1.0) - faraday-http-cache (2.5.1) + faraday-http-cache (2.6.1) faraday (>= 0.8) faraday-httpclient (1.0.1) - faraday-multipart (1.1.0) + faraday-multipart (1.2.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 (2.5.2) + rexml (>= 3.4.4) 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) + public_suffix (7.0.2) + rchardet (1.10.0) + rexml (3.4.4) ruby2_keywords (0.0.5) - rubyzip (2.4.1) - sawyer (0.9.2) + sawyer (0.9.3) 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 @@ -257,7 +81,6 @@ PLATFORMS DEPENDENCIES bundler (~> 2.6.3) danger (~> 8.2.0) - fastlane (>= 2.217.0, < 2.219.0) BUNDLED WITH 2.6.3 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). From 63d13aafcb2d6010a4329fd317d730e7efd160a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Feb 2026 17:51:25 +0100 Subject: [PATCH 19/52] feat: phase 1 --- Sources/Container/Async/AsyncContainer.swift | 44 ++------ Sources/Container/Sync/Container.swift | 44 ++------ Sources/Models/Async/AsyncRegistration.swift | 20 ++-- Sources/Models/RegistrationIdentifier.swift | 30 +++-- Sources/Models/Sync/Registration.swift | 20 ++-- .../Async/AsyncDependencyResolving.swift | 104 +++++------------- .../Resolution/Sync/DependencyResolving.swift | 103 +++++------------ .../Container/Async/AsyncArgumentTests.swift | 26 ++--- Tests/Container/Async/AsyncComplexTests.swift | 8 +- Tests/Container/Sync/ArgumentTests.swift | 20 ++-- .../AutoregistrationWithArgumentTest.swift | 22 ++-- Tests/Container/Sync/ComplexTests.swift | 8 +- 12 files changed, 148 insertions(+), 301 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index 24f1131..fad0418 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -105,22 +105,6 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin // MARK: Resolve dependency - /// 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 - /// 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) - - let registration = try getRegistration(with: identifier) - - return try await getDependency(from: registration, with: argument) as Dependency - } - /// Resolve a dependency that was previously registered with `register` method /// /// If a dependency of the given type wasn't registered before this method call @@ -136,36 +120,24 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin 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 /// + /// 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 - /// - arguments: Aarguments that will passed as an input parameter to the factory method that was defined with `register` method - public func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2) async throws -> Dependency { - let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + 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, with: (argument1, argument2)) as Dependency - } - - /// Resolve a dependency that was previously registered with `register` method - /// - /// 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 - /// - arguments: Aarguments that will passed as an input parameter to the factory method that was defined with `register` method - public func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async throws -> Dependency { - let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) - - let registration = try getRegistration(with: identifier) + // 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: (argument1, argument2, argument3)) as Dependency + return try await getDependency(from: registration, with: argumentsTuple) as Dependency } } diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 6d46030..10e69d1 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -101,22 +101,6 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency // MARK: Resolve dependency - /// 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 - /// 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) - - let registration = try getRegistration(with: identifier) - - return try getDependency(from: registration, with: argument) as Dependency - } - /// Resolve a dependency that was previously registered with `register` method /// /// If a dependency of the given type wasn't registered before this method call @@ -132,36 +116,24 @@ open class Container: DependencyAutoregistering, DependencyResolving, 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 /// + /// 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 - /// - arguments: Arguments that will passed as an input parameter to the factory method that was defined with `register` method - open func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2) throws -> Dependency { - let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + 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, with: (argument1, argument2)) as Dependency - } - - /// Resolve a dependency that was previously registered with `register` method - /// - /// 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 - /// - arguments: Arguments that will passed as an input parameter to the factory method that was defined with `register` method - open func tryResolve(type: Dependency.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) throws -> Dependency { - let identifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) - - let registration = try getRegistration(with: identifier) + // Pack arguments into a tuple for storage - this matches how Registration expects them + let argumentsTuple = (repeat each arguments) - return try getDependency(from: registration, with: (argument1, argument2, argument3)) as Dependency + return try getDependency(from: registration, with: argumentsTuple) as Dependency } } diff --git a/Sources/Models/Async/AsyncRegistration.swift b/Sources/Models/Async/AsyncRegistration.swift index 39c0f4d..0c4d89f 100644 --- a/Sources/Models/Async/AsyncRegistration.swift +++ b/Sources/Models/Async/AsyncRegistration.swift @@ -16,21 +16,21 @@ 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) + init(type: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument) async -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: 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)") + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept an argument of type \(Swift.type(of: arg))") } return await factory(resolver, argument) @@ -38,14 +38,14 @@ struct AsyncRegistration: Sendable { } /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2) async -> T) { - let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + init(type: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2) async -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self) identifier = registrationIdentifier self.scope = scope asyncRegistrationFactory = { resolver, arg in guard let arguments = arg as? (Argument1, Argument2) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self))") + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") } return await factory(resolver, arguments.0, arguments.1) @@ -53,14 +53,14 @@ struct AsyncRegistration: Sendable { } /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2, Argument3) async -> T) { - let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) + init(type: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2, Argument3) async -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self, Argument3.self) identifier = registrationIdentifier self.scope = scope asyncRegistrationFactory = { resolver, arg in guard let arguments = arg as? (Argument1, Argument2, Argument3) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self), \(Argument3.self))") + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") } return await factory(resolver, arguments.0, arguments.1, arguments.2) diff --git a/Sources/Models/RegistrationIdentifier.swift b/Sources/Models/RegistrationIdentifier.swift index f3e39f2..e19edd8 100644 --- a/Sources/Models/RegistrationIdentifier.swift +++ b/Sources/Models/RegistrationIdentifier.swift @@ -7,26 +7,36 @@ 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: Sendable { let typeIdentifier: 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 + init(type: Dependency.Type, argumentTypes: repeat (each Argument).Type) { typeIdentifier = ObjectIdentifier(type) - argumentIdentifiers = [ObjectIdentifier(Argument.self)] - } - init(type: Dependency.Type, argument1 _: Argument1.Type, argument2 _: Argument2.Type) { - typeIdentifier = ObjectIdentifier(type) - argumentIdentifiers = [ObjectIdentifier(Argument1.self), ObjectIdentifier(Argument2.self)] - } + var identifiers: [ObjectIdentifier] = [] + repeat identifiers.append(ObjectIdentifier((each Argument).self)) - init(type: Dependency.Type, argument1 _: Argument1.Type, argument2 _: Argument2.Type, argument3 _: Argument3.Type) { - typeIdentifier = ObjectIdentifier(type) - argumentIdentifiers = [ObjectIdentifier(Argument1.self), ObjectIdentifier(Argument2.self), ObjectIdentifier(Argument3.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) argumentIdentifiers = [] diff --git a/Sources/Models/Sync/Registration.swift b/Sources/Models/Sync/Registration.swift index 4e0c245..3ea8588 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -14,21 +14,21 @@ struct Registration { 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 (DependencyResolving, Argument) -> T) { - let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) + init(type: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument) -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: 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)") + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept an argument of type \(Swift.type(of: arg))") } return factory(resolver, argument) @@ -36,14 +36,14 @@ struct Registration { } /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2) -> T) { - let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self) + init(type: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2) -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self) identifier = registrationIdentifier self.scope = scope self.factory = { resolver, arg in guard let arguments = arg as? (Argument1, Argument2) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self))") + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") } return factory(resolver, arguments.0, arguments.1) @@ -51,14 +51,14 @@ struct Registration { } /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2, Argument3) -> T) { - let registrationIdentifier = RegistrationIdentifier(type: type, argument1: Argument1.self, argument2: Argument2.self, argument3: Argument3.self) + init(type: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2, Argument3) -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self, Argument3.self) identifier = registrationIdentifier self.scope = scope self.factory = { resolver, arg in guard let arguments = arg as? (Argument1, Argument2, Argument3) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type (\(Argument1.self), \(Argument2.self), \(Argument3.self))") + throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") } return factory(resolver, arguments.0, arguments.1, arguments.2) diff --git a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift index f5f5eaa..a1b185d 100644 --- a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift +++ b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift @@ -15,34 +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 - - /// Resolve a dependency with two 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 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 an input parameters to the factory method - func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) async throws -> T - - /// Resolve a dependency with three 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 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 an input parameters to the factory method - func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) 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 { @@ -52,78 +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) - } - - /// 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) async -> T { - try! await 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: - /// - 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) - } - - /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) async -> T { - try! await tryResolve(type: type, argument1: argument1, argument2: argument2) - } - - /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(argument1: Argument1, argument2: Argument2) async -> T { - await resolve(type: T.self, argument1: argument1, argument2: argument2) + func resolve() async -> Dependency { + await resolve(type: Dependency.self) } - /// Resolve a dependency with three variable arguments 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 arguments of different types than expected are 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 - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) async -> T { - try! await tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) + /// - 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, repeat each arguments) } - /// Resolve a dependency with three variable arguments 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 arguments of different types than expected are 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: - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) async -> T { - await resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) + /// - 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, repeat each arguments) } } diff --git a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift index 3aaa88d..811748a 100644 --- a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift +++ b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift @@ -15,34 +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 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) throws -> T - - /// Resolve a dependency with two 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 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 an input parameters to the factory method - func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2) throws -> T - - /// Resolve a dependency with three 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 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 an input parameters to the factory method - func tryResolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) 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) throws -> Dependency } public extension DependencyResolving { @@ -52,77 +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() -> T { - resolve(type: T.self) - } - - /// 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: - /// - 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) - } - - /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(type: T.Type, argument1: Argument1, argument2: Argument2) -> T { - try! tryResolve(type: type, argument1: argument1, argument2: argument2) - } - - /// Resolve a dependency with two 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 arguments of different types than expected are passed, a runtime error occurs - /// - /// - Parameters: - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(argument1: Argument1, argument2: Argument2) -> T { - resolve(type: T.self, argument1: argument1, argument2: argument2) + func resolve() -> Dependency { + resolve(type: Dependency.self) } - /// Resolve a dependency with three variable arguments 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 arguments of different types than expected are 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 - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(type: T.Type, argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { - try! tryResolve(type: type, argument1: argument1, argument2: argument2, argument3: argument3) + /// - 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, repeat each arguments) } - /// Resolve a dependency with three variable arguments 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 arguments of different types than expected are 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: - /// - arguments: Arguments that will be passed as an input parameters to the factory method - func resolve(argument1: Argument1, argument2: Argument2, argument3: Argument3) -> T { - resolve(type: T.self, argument1: argument1, argument2: argument2, argument3: argument3) + /// - 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, repeat each arguments) } } diff --git a/Tests/Container/Async/AsyncArgumentTests.swift b/Tests/Container/Async/AsyncArgumentTests.swift index a3f0c70..70d4ce9 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(argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -26,7 +26,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(argument: argument) + let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -39,7 +39,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument = 48 do { - _ = try await container.tryResolve(type: SimpleDependency.self, argument: argument) + _ = try await container.tryResolve(type: SimpleDependency.self, argument) XCTFail("Expected to throw error") } catch { @@ -65,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, argument) XCTFail("Expected to throw error") } catch { @@ -89,7 +89,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithAsyncInitWithParameter = await container.resolve(argument: argument) + let resolvedDependency: DependencyWithAsyncInitWithParameter = await container.resolve(argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -101,7 +101,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = await container.resolve(argument1: argument1, argument2: argument2) + let resolvedDependency: DependencyWithTwoArguments = await container.resolve(argument1, argument2) XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") @@ -114,7 +114,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = await container.resolve(argument1: argument1, argument2: argument2) + let resolvedDependency: DependencyWithTwoArguments = await container.resolve(argument1, argument2) XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") @@ -129,7 +129,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument2 = "test" do { - _ = try await container.tryResolve(type: DependencyWithTwoArguments.self, argument1: argument1, argument2: argument2) + _ = try await container.tryResolve(type: DependencyWithTwoArguments.self, argument1, argument2) XCTFail("Expected to throw error") } catch { @@ -155,7 +155,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = await container.resolve(argument1: argument1, argument2: argument2, argument3: argument3) + let resolvedDependency: DependencyWithThreeArguments = await container.resolve(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") @@ -170,7 +170,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = await container.resolve(argument1: argument1, argument2: argument2, argument3: argument3) + let resolvedDependency: DependencyWithThreeArguments = await container.resolve(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") @@ -187,7 +187,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument3 = 42 do { - _ = try await container.tryResolve(type: DependencyWithThreeArguments.self, argument1: argument1, argument2: argument2, argument3: argument3) + _ = try await container.tryResolve(type: DependencyWithThreeArguments.self, argument1, argument2, argument3) XCTFail("Expected to throw error") } catch { @@ -212,7 +212,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithAsyncInitWithTwoArguments = await container.resolve(argument1: argument1, argument2: argument2) + let resolvedDependency: DependencyWithAsyncInitWithTwoArguments = await container.resolve(argument1, argument2) XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") @@ -226,7 +226,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithAsyncInitWithThreeArguments = await container.resolve(argument1: argument1, argument2: argument2, argument3: argument3) + let resolvedDependency: DependencyWithAsyncInitWithThreeArguments = await container.resolve(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") diff --git a/Tests/Container/Async/AsyncComplexTests.swift b/Tests/Container/Async/AsyncComplexTests.swift index 4ddd0aa..4253283 100644 --- a/Tests/Container/Async/AsyncComplexTests.swift +++ b/Tests/Container/Async/AsyncComplexTests.swift @@ -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, argumentDependency1) + let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, 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, argumentDependency1) + let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, 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 c61aae5..98f87c2 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(argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -26,7 +26,7 @@ final class ContainerArgumentTests: DITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(argument: argument) + let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -39,7 +39,7 @@ final class ContainerArgumentTests: DITestCase { let argument = 48 XCTAssertThrowsError( - try container.tryResolve(type: SimpleDependency.self, argument: argument), + try container.tryResolve(type: SimpleDependency.self, argument), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { @@ -64,7 +64,7 @@ final class ContainerArgumentTests: DITestCase { let argument = 48 XCTAssertThrowsError( - try container.tryResolve(type: DependencyWithValueTypeParameter.self, argument: argument), + try container.tryResolve(type: DependencyWithValueTypeParameter.self, argument), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { @@ -88,7 +88,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = container.resolve(argument1: argument1, argument2: argument2) + let resolvedDependency: DependencyWithTwoArguments = container.resolve(argument1, argument2) XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") @@ -101,7 +101,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = container.resolve(argument1: argument1, argument2: argument2) + let resolvedDependency: DependencyWithTwoArguments = container.resolve(argument1, argument2) XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") @@ -116,7 +116,7 @@ final class ContainerArgumentTests: DITestCase { let argument2 = "test" XCTAssertThrowsError( - try container.tryResolve(type: DependencyWithTwoArguments.self, argument1: argument1, argument2: argument2), + try container.tryResolve(type: DependencyWithTwoArguments.self, argument1, argument2), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { @@ -141,7 +141,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = container.resolve(argument1: argument1, argument2: argument2, argument3: argument3) + let resolvedDependency: DependencyWithThreeArguments = container.resolve(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") @@ -156,7 +156,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = container.resolve(argument1: argument1, argument2: argument2, argument3: argument3) + let resolvedDependency: DependencyWithThreeArguments = container.resolve(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") @@ -173,7 +173,7 @@ final class ContainerArgumentTests: DITestCase { let argument3 = 42 XCTAssertThrowsError( - try container.tryResolve(type: DependencyWithThreeArguments.self, argument1: argument1, argument2: argument2, argument3: argument3), + try container.tryResolve(type: DependencyWithThreeArguments.self, argument1, argument2, argument3), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { diff --git a/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift b/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift index 728a887..a075d04 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(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(argument) + let secondResolved: DependencyWithParameter2 = container.resolve(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(argument) + let secondResolved: DependencyWithParameter2 = container.resolve(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(argument) + let secondResolved: DependencyWithParameter3 = container.resolve(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(argument) + let secondResolved: DependencyWithParameter3 = container.resolve(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(argument) + let secondResolved: DependencyWithParameter3 = container.resolve(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/ComplexTests.swift b/Tests/Container/Sync/ComplexTests.swift index 333b678..10d6cd5 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, argumentDependency1) + let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, 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, argumentDependency1) + let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, argumentDependency2) XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Resolved different instances") XCTAssertTrue(resolvedDependency1.subDependency === resolvedDependency2.subDependency, "Resolved different instances") From 1a71427f92a0f74253c0f9a9e0fc5ddf64ad406b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Feb 2026 17:58:07 +0100 Subject: [PATCH 20/52] feat: phase 2 --- Sources/Models/Async/AsyncRegistration.swift | 46 +++++--------------- Sources/Models/Sync/Registration.swift | 46 +++++--------------- 2 files changed, 20 insertions(+), 72 deletions(-) diff --git a/Sources/Models/Async/AsyncRegistration.swift b/Sources/Models/Async/AsyncRegistration.swift index 0c4d89f..0b6c4ac 100644 --- a/Sources/Models/Async/AsyncRegistration.swift +++ b/Sources/Models/Async/AsyncRegistration.swift @@ -22,48 +22,22 @@ struct AsyncRegistration: Sendable { 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: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument) async -> Dependency) { - let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: 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. + 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 \(Swift.type(of: arg))") + 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) - } - } - - /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved - init(type: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2) async -> Dependency) { - let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self) - - identifier = registrationIdentifier - self.scope = scope - asyncRegistrationFactory = { resolver, arg in - guard let arguments = arg as? (Argument1, Argument2) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") - } - - return await factory(resolver, arguments.0, arguments.1) - } - } - - /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved - init(type: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument1, Argument2, Argument3) async -> Dependency) { - let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self, Argument3.self) - - identifier = registrationIdentifier - self.scope = scope - asyncRegistrationFactory = { resolver, arg in - guard let arguments = arg as? (Argument1, Argument2, Argument3) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") - } - - return await factory(resolver, arguments.0, arguments.1, arguments.2) + return await factory(resolver, repeat each arguments) } } } diff --git a/Sources/Models/Sync/Registration.swift b/Sources/Models/Sync/Registration.swift index 3ea8588..64f3197 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -20,48 +20,22 @@ struct Registration { 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: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument) -> Dependency) { - let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: 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. + 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 \(Swift.type(of: arg))") + 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) - } - } - - /// Initializer for registrations that expect two variable arguments passed to the factory closure when the dependency is being resolved - init(type: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2) -> Dependency) { - let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self) - - identifier = registrationIdentifier - self.scope = scope - self.factory = { resolver, arg in - guard let arguments = arg as? (Argument1, Argument2) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") - } - - return factory(resolver, arguments.0, arguments.1) - } - } - - /// Initializer for registrations that expect three variable arguments passed to the factory closure when the dependency is being resolved - init(type: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, Argument1, Argument2, Argument3) -> Dependency) { - let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: Argument1.self, Argument2.self, Argument3.self) - - identifier = registrationIdentifier - self.scope = scope - self.factory = { resolver, arg in - guard let arguments = arg as? (Argument1, Argument2, Argument3) else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))") - } - - return factory(resolver, arguments.0, arguments.1, arguments.2) + return factory(resolver, repeat each arguments) } } } From c0be98cbefac6065bb68d3531537dcdf8988cf3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 3 Feb 2026 12:14:20 +0100 Subject: [PATCH 21/52] feat: phase 3 --- CHANGELOG.md | 3 +- Sources/Container/Async/AsyncContainer.swift | 41 +-------- Sources/Container/Sync/Container.swift | 41 +-------- .../Async/AsyncDependencyRegistering.swift | 86 +++---------------- .../Sync/DependencyAutoregistering.swift | 12 +-- .../Sync/DependencyRegistering.swift | 85 ++---------------- .../Container/Async/AsyncArgumentTests.swift | 2 +- Tests/Container/Async/AsyncBaseTests.swift | 4 +- Tests/Container/Async/AsyncComplexTests.swift | 2 +- Tests/Container/Sync/ArgumentTests.swift | 2 +- Tests/Container/Sync/BaseTests.swift | 4 +- 11 files changed, 43 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd90dc9..0fc24e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,10 @@ __Sections__ ## [1.0.5] -### Fixed +### Added - Support for multiple arguments. Code clean up. +- Using parameter packs for multiple arguments ## [1.0.4] diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index fad0418..9e4ccd9 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -52,44 +52,11 @@ 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 - /// - /// 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. 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 FactoryWithOneArgument) async { - let registration = AsyncRegistration(type: type, scope: .new, factory: factory) - - registrations[registration.identifier] = registration - } - - // MARK: Register dependency with two arguments - - /// Register a dependency with two arguments - /// - /// 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 FactoryWithTwoArguments) async { - let registration = AsyncRegistration(type: type, scope: .new, factory: factory) - - registrations[registration.identifier] = registration - } - - // MARK: Register dependency with three arguments - - /// Register a dependency with three arguments + /// 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 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. @@ -97,7 +64,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// - 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 FactoryWithThreeArguments) async { + public func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) async { let registration = AsyncRegistration(type: type, scope: .new, factory: factory) registrations[registration.identifier] = registration diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 10e69d1..6e6a9ff 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -48,44 +48,11 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency sharedInstances[registration.identifier] = nil } - // MARK: Register dependency with argument, Autoregister dependency with argument + // MARK: Register dependency with arguments - /// Register a dependency with an argument - /// - /// 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. 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 FactoryWithOneArgument) { - let registration = Registration(type: type, scope: .new, factory: factory) - - registrations[registration.identifier] = registration - } - - // MARK: Register dependency with two arguments - - /// Register a dependency with two arguments - /// - /// 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 FactoryWithTwoArguments) { - let registration = Registration(type: type, scope: .new, factory: factory) - - registrations[registration.identifier] = registration - } - - // MARK: Register dependency with three arguments - - /// Register a dependency with three arguments + /// 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 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. @@ -93,7 +60,7 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// - 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 FactoryWithThreeArguments) { + open func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) { let registration = Registration(type: type, scope: .new, factory: factory) registrations[registration.identifier] = registration diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index 7e240bb..4c8282b 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -9,15 +9,12 @@ import Foundation /// A type that is able to register a dependency public protocol AsyncDependencyRegistering { - /// Factory closures that instantiates the required dependency + /// Factory closure that instantiates the required dependency without arguments typealias Factory = @Sendable (any AsyncDependencyResolving) async -> Dependency - - typealias FactoryWithOneArgument = @Sendable (any AsyncDependencyResolving, Argument) async -> Dependency - - typealias FactoryWithTwoArguments = @Sendable (any AsyncDependencyResolving, Argument1, Argument2) async -> Dependency - - typealias FactoryWithThreeArguments = @Sendable (any AsyncDependencyResolving, Argument1, Argument2, Argument3) 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 /// /// - Parameters: @@ -26,30 +23,9 @@ 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 - /// - /// 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. 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 FactoryWithOneArgument) async - - /// Register a dependency with two variable arguments - /// - /// 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 as the behaviour for resolving shared instances with arguments is undefined. - /// - /// - 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 FactoryWithTwoArguments) async - - /// Register a dependency with three variable arguments + /// 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. @@ -57,7 +33,7 @@ public protocol AsyncDependencyRegistering { /// - 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 FactoryWithThreeArguments) async + func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) async } // MARK: Overloaded factory methods @@ -69,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: @@ -87,47 +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. 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 FactoryWithOneArgument) async { - await register(type: Dependency.self, factory: factory) - } - - /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type - /// - /// 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 FactoryWithTwoArguments) async { - await register(type: Dependency.self, factory: factory) - } - - /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// 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 FactoryWithThreeArguments) 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 ca32ff7..d630384 100644 --- a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift @@ -340,7 +340,7 @@ public extension DependencyAutoregistering { argument: Argument.Type, initializer: @escaping (Argument) -> Dependency ) { - let factory: FactoryWithOneArgument = { _, argument in + let factory: FactoryWithArguments = { _, argument in initializer(argument) } @@ -365,7 +365,7 @@ public extension DependencyAutoregistering { argument: Argument.Type, initializer: @escaping (Argument, Parameter) -> Dependency ) { - let factory: FactoryWithOneArgument = { resolver, argument in + let factory: FactoryWithArguments = { resolver, argument in initializer( argument, resolver.resolve(type: Parameter.self) @@ -392,7 +392,7 @@ public extension DependencyAutoregistering { argument: Argument.Type, initializer: @escaping (Parameter, Argument) -> Dependency ) { - let factory: FactoryWithOneArgument = { resolver, argument in + let factory: FactoryWithArguments = { resolver, argument in initializer( resolver.resolve(type: Parameter.self), argument @@ -419,7 +419,7 @@ public extension DependencyAutoregistering { argument: Argument.Type, initializer: @escaping (Argument, Parameter1, Parameter2) -> Dependency ) { - let factory: FactoryWithOneArgument = { resolver, argument in + let factory: FactoryWithArguments = { resolver, argument in initializer( argument, resolver.resolve(type: Parameter1.self), @@ -447,7 +447,7 @@ public extension DependencyAutoregistering { argument: Argument.Type, initializer: @escaping (Parameter1, Argument, Parameter2) -> Dependency ) { - let factory: FactoryWithOneArgument = { resolver, argument in + let factory: FactoryWithArguments = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), argument, @@ -475,7 +475,7 @@ public extension DependencyAutoregistering { argument: Argument.Type, initializer: @escaping (Parameter1, Parameter2, Argument) -> Dependency ) { - let factory: FactoryWithOneArgument = { resolver, argument in + let factory: FactoryWithArguments = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), resolver.resolve(type: Parameter2.self), diff --git a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift index 4c98a62..7c4f932 100644 --- a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift @@ -9,17 +9,11 @@ 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 the given variable argument - typealias FactoryWithOneArgument = (DependencyResolving, Argument) -> Dependency - - /// Factory closure that instantiates the required dependency with two given variable arguments - typealias FactoryWithTwoArguments = (DependencyResolving, Argument1, Argument2) -> Dependency - - /// Factory closure that instantiates the required dependency with three given variable arguments - typealias FactoryWithThreeArguments = (DependencyResolving, Argument1, Argument2, Argument3) -> Dependency + /// Factory closure that instantiates the required dependency with variable arguments (1-3 supported) + typealias FactoryWithArguments = (DependencyResolving, repeat each Argument) -> Dependency /// Register a dependency /// @@ -29,30 +23,9 @@ public protocol DependencyRegistering { /// - 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 a variable argument - /// - /// 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. 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 FactoryWithOneArgument) - - /// Register a dependency with two variable arguments - /// - /// 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 FactoryWithTwoArguments) - - /// Register a dependency with three variable arguments + /// 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. @@ -60,7 +33,7 @@ public protocol DependencyRegistering { /// - 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 FactoryWithThreeArguments) + func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) } // MARK: Overloaded factory methods @@ -72,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: @@ -90,47 +54,16 @@ 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 - /// - /// - 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) - } - - /// 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. 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 FactoryWithOneArgument) { - register(type: Dependency.self, factory: factory) - } - - /// Register a dependency with two variable arguments. The type of the dependency is determined implicitly based on the factory closure return type - /// - /// 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 FactoryWithTwoArguments) { - register(type: Dependency.self, factory: factory) - } - - /// Register a dependency with three variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// 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 FactoryWithThreeArguments) { + func register(factory: @escaping FactoryWithArguments) { register(type: Dependency.self, factory: factory) } } diff --git a/Tests/Container/Async/AsyncArgumentTests.swift b/Tests/Container/Async/AsyncArgumentTests.swift index 70d4ce9..3b9fcea 100644 --- a/Tests/Container/Async/AsyncArgumentTests.swift +++ b/Tests/Container/Async/AsyncArgumentTests.swift @@ -32,7 +32,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } func testUnmatchingArgumentType_ZeroArguments() async { - await container.register { _ -> SimpleDependency in + await container.register(in: .shared) { _ -> SimpleDependency in SimpleDependency() } 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 4253283..89527d0 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() } diff --git a/Tests/Container/Sync/ArgumentTests.swift b/Tests/Container/Sync/ArgumentTests.swift index 98f87c2..f792799 100644 --- a/Tests/Container/Sync/ArgumentTests.swift +++ b/Tests/Container/Sync/ArgumentTests.swift @@ -32,7 +32,7 @@ final class ContainerArgumentTests: DITestCase { } func testUnmatchingArgumentType_ZeroArguments() { - container.register { _ -> SimpleDependency in + container.register(in: .shared) { _ -> SimpleDependency in SimpleDependency() } 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() } From 709d52d1b09fc4eb5960572e43bd9a57aac54a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Tue, 3 Feb 2026 13:41:08 +0100 Subject: [PATCH 22/52] feat: autoregistering changes --- .../Sync/DependencyAutoregistering.swift | 186 ++---------------- 1 file changed, 11 insertions(+), 175 deletions(-) diff --git a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift index d630384..7ce3339 100644 --- a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift @@ -9,76 +9,19 @@ 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, 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. /// - /// - 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( - type: Dependency.Type, - in scope: DependencyScope, - initializer: @escaping () -> Dependency - ) - - /// Autoregister a dependency with the provided initializer method that has one parameter which is a dependency that is registered within the same 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 - /// - 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, - in scope: DependencyScope, - initializer: @escaping (Parameter) -> Dependency - ) - - /// Autoregister a dependency with the provided initializer method that has two parameters which are dependencies that are registered within the same 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 - /// - 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, - in scope: DependencyScope, - initializer: @escaping (Parameter1, Parameter2) -> Dependency - ) - - /// Autoregister a dependency with the provided initializer method that has three parameters which are dependencies that are registered within the same 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 - /// - 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, - in scope: DependencyScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3) -> Dependency - ) - - /// Autoregister a dependency with the provided initializer method that has four parameters which are 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 (Parameter1, Parameter2, Parameter3, Parameter4) -> Dependency - ) - - /// Autoregister a dependency with the provided initializer method that has five parameters which are dependencies that are registered within the same 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 - /// - 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, - in scope: DependencyScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4, Parameter5) -> Dependency + initializer: @escaping (repeat each Parameter) -> Dependency ) // MARK: Autoregister with variable argument @@ -198,129 +141,22 @@ public protocol DependencyAutoregistering: DependencyRegistering { // 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. /// - /// - 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( - type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping () -> Dependency - ) { - let factory: Factory = { _ in - initializer() - } - - 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 + /// 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 (Parameter) -> Dependency + initializer: @escaping (repeat each Parameter) -> Dependency ) { let factory: Factory = { resolver in - initializer( - resolver.resolve(type: Parameter.self) - ) - } - - register(type: type, in: scope, factory: factory) - } - - /// Autoregister a dependency with the provided initializer method that has two parameters which are dependencies that are registered within the same container - /// - /// - 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( - type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2) -> Dependency - ) { - let factory: Factory = { resolver in - initializer( - resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self) - ) - } - - register(type: type, in: scope, factory: factory) - } - - /// Autoregister a dependency with the provided initializer method that has three parameters which are dependencies that are registered within the same container - /// - /// - 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( - type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3) -> Dependency - ) { - let factory: Factory = { resolver in - initializer( - resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self), - resolver.resolve(type: Parameter3.self) - ) - } - - register(type: type, in: scope, factory: factory) - } - - /// Autoregister a dependency with the provided initializer method that has four parameters which are dependencies that are registered within the same container - /// - /// - 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( - type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4) -> Dependency - ) { - let factory: Factory = { resolver in - initializer( - resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self), - resolver.resolve(type: Parameter3.self), - resolver.resolve(type: Parameter4.self) - ) - } - - register(type: type, in: scope, factory: factory) - } - - /// Autoregister a dependency with the provided initializer method that has five parameters which are dependencies that are registered within the same container - /// - /// - 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( - type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4, Parameter5) -> Dependency - ) { - let factory: Factory = { resolver 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) - ) + initializer(repeat resolver.resolve(type: (each Parameter).self)) } register(type: type, in: scope, factory: factory) From 8315b2f729f8328593aa053d693b6e270c3ead88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 9 Feb 2026 13:27:12 +0100 Subject: [PATCH 23/52] feat: arguments parameter title --- Sources/Container/Async/AsyncContainer.swift | 2 +- Sources/Container/Sync/Container.swift | 2 +- .../Async/AsyncDependencyResolving.swift | 10 +++---- .../Resolution/Sync/DependencyResolving.swift | 10 +++---- .../Container/Async/AsyncArgumentTests.swift | 26 +++++++++---------- Tests/Container/Async/AsyncComplexTests.swift | 8 +++--- Tests/Container/Sync/ArgumentTests.swift | 20 +++++++------- .../AutoregistrationWithArgumentTest.swift | 22 ++++++++-------- Tests/Container/Sync/ComplexTests.swift | 8 +++--- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index 9e4ccd9..a04ed96 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -96,7 +96,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// - 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) - public func tryResolve(type: Dependency.Type, _ arguments: repeat each Argument) async throws -> Dependency { + 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) diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index 6e6a9ff..bc2bd1a 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -92,7 +92,7 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// - 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) - open func tryResolve(type: Dependency.Type, _ arguments: repeat each Argument) throws -> Dependency { + 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) diff --git a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift index a1b185d..a10a102 100644 --- a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift +++ b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift @@ -26,7 +26,7 @@ public protocol AsyncDependencyResolving { /// - 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) async throws -> Dependency + func tryResolve(type: Dependency.Type, arguments: repeat each Argument) async throws -> Dependency } public extension AsyncDependencyResolving { @@ -56,8 +56,8 @@ public extension AsyncDependencyResolving { /// - 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 resolve(type: Dependency.Type, _ arguments: repeat each Argument) async -> Dependency { - try! await tryResolve(type: type, repeat each arguments) + func resolve(type: Dependency.Type, arguments: repeat each Argument) async -> Dependency { + try! await tryResolve(type: type, arguments: repeat each arguments) } /// Resolve a dependency with variable arguments that was previously registered within the container. @@ -69,7 +69,7 @@ public extension AsyncDependencyResolving { /// /// - Parameters: /// - 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, repeat each arguments) + 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 811748a..a3af0a5 100644 --- a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift +++ b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift @@ -26,7 +26,7 @@ public protocol DependencyResolving { /// - 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 + func tryResolve(type: Dependency.Type, arguments: repeat each Argument) throws -> Dependency } public extension DependencyResolving { @@ -56,8 +56,8 @@ public extension DependencyResolving { /// - 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 resolve(type: Dependency.Type, _ arguments: repeat each Argument) -> Dependency { - try! tryResolve(type: type, repeat each arguments) + 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. @@ -69,7 +69,7 @@ public extension DependencyResolving { /// /// - 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, repeat each arguments) + func resolve(arguments: repeat each Argument) -> Dependency { + resolve(type: Dependency.self, arguments: repeat each arguments) } } diff --git a/Tests/Container/Async/AsyncArgumentTests.swift b/Tests/Container/Async/AsyncArgumentTests.swift index 3b9fcea..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) + let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -26,7 +26,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(argument) + let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -39,7 +39,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument = 48 do { - _ = try await container.tryResolve(type: SimpleDependency.self, argument) + _ = try await container.tryResolve(type: SimpleDependency.self, arguments: argument) XCTFail("Expected to throw error") } catch { @@ -65,7 +65,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument = 48 do { - _ = try await container.tryResolve(type: DependencyWithValueTypeParameter.self, argument) + _ = try await container.tryResolve(type: DependencyWithValueTypeParameter.self, arguments: argument) XCTFail("Expected to throw error") } catch { @@ -89,7 +89,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithAsyncInitWithParameter = await container.resolve(argument) + let resolvedDependency: DependencyWithAsyncInitWithParameter = await container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -101,7 +101,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = await container.resolve(argument1, argument2) + 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") @@ -114,7 +114,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = await container.resolve(argument1, argument2) + 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") @@ -129,7 +129,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument2 = "test" do { - _ = try await container.tryResolve(type: DependencyWithTwoArguments.self, argument1, argument2) + _ = try await container.tryResolve(type: DependencyWithTwoArguments.self, arguments: argument1, argument2) XCTFail("Expected to throw error") } catch { @@ -155,7 +155,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = await container.resolve(argument1, argument2, argument3) + 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") @@ -170,7 +170,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = await container.resolve(argument1, argument2, argument3) + 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") @@ -187,7 +187,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument3 = 42 do { - _ = try await container.tryResolve(type: DependencyWithThreeArguments.self, argument1, argument2, argument3) + _ = try await container.tryResolve(type: DependencyWithThreeArguments.self, arguments: argument1, argument2, argument3) XCTFail("Expected to throw error") } catch { @@ -212,7 +212,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithAsyncInitWithTwoArguments = await container.resolve(argument1, argument2) + 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") @@ -226,7 +226,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithAsyncInitWithThreeArguments = await container.resolve(argument1, argument2, argument3) + 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") diff --git a/Tests/Container/Async/AsyncComplexTests.swift b/Tests/Container/Async/AsyncComplexTests.swift index 89527d0..be192fe 100644 --- a/Tests/Container/Async/AsyncComplexTests.swift +++ b/Tests/Container/Async/AsyncComplexTests.swift @@ -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, argumentDependency1) - let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, 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, argumentDependency1) - let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, 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 f792799..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) + let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -26,7 +26,7 @@ final class ContainerArgumentTests: DITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(argument) + let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -39,7 +39,7 @@ final class ContainerArgumentTests: DITestCase { let argument = 48 XCTAssertThrowsError( - try container.tryResolve(type: SimpleDependency.self, argument), + try container.tryResolve(type: SimpleDependency.self, arguments: argument), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { @@ -64,7 +64,7 @@ final class ContainerArgumentTests: DITestCase { let argument = 48 XCTAssertThrowsError( - try container.tryResolve(type: DependencyWithValueTypeParameter.self, argument), + try container.tryResolve(type: DependencyWithValueTypeParameter.self, arguments: argument), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { @@ -88,7 +88,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = container.resolve(argument1, argument2) + 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") @@ -101,7 +101,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" - let resolvedDependency: DependencyWithTwoArguments = container.resolve(argument1, argument2) + 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") @@ -116,7 +116,7 @@ final class ContainerArgumentTests: DITestCase { let argument2 = "test" XCTAssertThrowsError( - try container.tryResolve(type: DependencyWithTwoArguments.self, argument1, argument2), + try container.tryResolve(type: DependencyWithTwoArguments.self, arguments: argument1, argument2), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { @@ -141,7 +141,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = container.resolve(argument1, argument2, argument3) + 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") @@ -156,7 +156,7 @@ final class ContainerArgumentTests: DITestCase { let argument1 = StructureDependency(property1: "test1") let argument2 = "test2" let argument3 = 42 - let resolvedDependency: DependencyWithThreeArguments = container.resolve(argument1, argument2, argument3) + 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") @@ -173,7 +173,7 @@ final class ContainerArgumentTests: DITestCase { let argument3 = 42 XCTAssertThrowsError( - try container.tryResolve(type: DependencyWithThreeArguments.self, argument1, argument2, argument3), + 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 { diff --git a/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift b/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift index a075d04..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) + 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) - let secondResolved: DependencyWithParameter2 = container.resolve(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) - let secondResolved: DependencyWithParameter2 = container.resolve(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) - let secondResolved: DependencyWithParameter3 = container.resolve(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) - let secondResolved: DependencyWithParameter3 = container.resolve(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) - let secondResolved: DependencyWithParameter3 = container.resolve(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/ComplexTests.swift b/Tests/Container/Sync/ComplexTests.swift index 10d6cd5..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, argumentDependency1) - let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, 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, argumentDependency1) - let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, 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") From db0837396ca46d1a4fd09fd5ac8d2512ce8bcfd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Fri, 20 Feb 2026 10:06:17 +0100 Subject: [PATCH 24/52] Apply suggestion from @TParizek Co-authored-by: TParizek --- .github/workflows/integrations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 1103e32..20a9e73 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -1,4 +1,4 @@ - ### Integrations workflow ### +### Integrations workflow ### # # Runs Danger checks, Swiftlint and tests. # From 204d1ee436490528ea4ccbeab5cd210e46ab055f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Fri, 20 Feb 2026 10:08:35 +0100 Subject: [PATCH 25/52] feat: any sendable --- Sources/Container/Async/AsyncContainer.swift | 4 ++-- Sources/Models/Async/AsyncRegistration.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index a04ed96..fa4dd1b 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() {} @@ -127,7 +127,7 @@ private extension AsyncContainer { } - func getDependency(from registration: AsyncRegistration, with argument: Any? = nil) async throws -> Dependency { + func getDependency(from registration: AsyncRegistration, with argument: (any Sendable)? = nil) async throws -> Dependency { switch registration.scope { case .shared: if let dependency = sharedInstances[registration.identifier] as? Dependency { diff --git a/Sources/Models/Async/AsyncRegistration.swift b/Sources/Models/Async/AsyncRegistration.swift index 0b6c4ac..8307e2f 100644 --- a/Sources/Models/Async/AsyncRegistration.swift +++ b/Sources/Models/Async/AsyncRegistration.swift @@ -7,7 +7,7 @@ import Foundation -typealias AsyncRegistrationFactory = @Sendable (any AsyncDependencyResolving, Any?) async throws -> any Sendable +typealias AsyncRegistrationFactory = @Sendable (any AsyncDependencyResolving, (any Sendable)?) async throws -> any Sendable /// Object that represents a registered dependency and stores a closure, i.e. a factory that returns the desired dependency struct AsyncRegistration: Sendable { From 0a8e775243931e66d98907a27641a3666642f8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Fri, 20 Feb 2026 10:32:57 +0100 Subject: [PATCH 26/52] chore: comments --- Sources/Container/Async/AsyncContainer.swift | 2 +- Sources/Container/Sync/Container.swift | 2 +- Sources/Models/Async/AsyncRegistration.swift | 2 +- Sources/Models/RegistrationIdentifier.swift | 9 ++++----- Sources/Models/Sync/Registration.swift | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index fa4dd1b..c57fc63 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -95,7 +95,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// /// - 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) + /// - 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) diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index bc2bd1a..f59fc12 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -91,7 +91,7 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// /// - 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) + /// - 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) diff --git a/Sources/Models/Async/AsyncRegistration.swift b/Sources/Models/Async/AsyncRegistration.swift index 8307e2f..b7eedf5 100644 --- a/Sources/Models/Async/AsyncRegistration.swift +++ b/Sources/Models/Async/AsyncRegistration.swift @@ -24,7 +24,7 @@ struct AsyncRegistration: Sendable { /// 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. + /// 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) diff --git a/Sources/Models/RegistrationIdentifier.swift b/Sources/Models/RegistrationIdentifier.swift index e19edd8..4a7dbc7 100644 --- a/Sources/Models/RegistrationIdentifier.swift +++ b/Sources/Models/RegistrationIdentifier.swift @@ -21,7 +21,7 @@ struct RegistrationIdentifier: Sendable { /// /// - Parameters: /// - type: Type of the dependency - /// - argumentTypes: Variadic argument types using parameter packs + /// - 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) @@ -49,11 +49,10 @@ extension RegistrationIdentifier: Hashable {} // MARK: Debug information extension RegistrationIdentifier: CustomStringConvertible { var description: String { - let argumentsDescription: String - if argumentIdentifiers.isEmpty { - argumentsDescription = "nil" + let argumentsDescription: String = if argumentIdentifiers.isEmpty { + "nil" } else { - argumentsDescription = argumentIdentifiers.map { $0.debugDescription }.joined(separator: ", ") + argumentIdentifiers.map { $0.debugDescription }.joined(separator: ", ") } return """ Type: \(typeIdentifier.debugDescription) diff --git a/Sources/Models/Sync/Registration.swift b/Sources/Models/Sync/Registration.swift index 64f3197..112a06b 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -22,7 +22,7 @@ struct Registration { /// 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. + /// 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) From 1a5335e9dcb991c5a013339ab5dd4809bf3aa9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:04:30 +0100 Subject: [PATCH 27/52] fix: install required bundler version before bundle install in CI Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/integrations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 20a9e73..d27de9b 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -35,6 +35,7 @@ jobs: # install dependencies from Gemfile & CocoaPods - name: Dependencies run: | + gem install bundler:2.6.3 bundle install # run Danger From fb05ed5935b794e392ae8ea5705b46555a5a967f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:07:04 +0100 Subject: [PATCH 28/52] fix: use sudo to install bundler in CI Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/integrations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index d27de9b..16f9362 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -35,7 +35,7 @@ jobs: # install dependencies from Gemfile & CocoaPods - name: Dependencies run: | - gem install bundler:2.6.3 + sudo gem install bundler:2.6.3 bundle install # run Danger From 5fabc6fc545ed209d652834eeec2429baa4cf005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:26:14 +0100 Subject: [PATCH 29/52] feat: mise --- .bundler-version | 1 - .github/workflows/integrations.yml | 16 +++--- .mise.toml | 2 + Gemfile | 7 --- Gemfile.lock | 86 ------------------------------ 5 files changed, 8 insertions(+), 104 deletions(-) delete mode 100644 .bundler-version create mode 100644 .mise.toml delete mode 100644 Gemfile delete mode 100644 Gemfile.lock diff --git a/.bundler-version b/.bundler-version deleted file mode 100644 index ec1cf33..0000000 --- a/.bundler-version +++ /dev/null @@ -1 +0,0 @@ -2.6.3 diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 16f9362..ff7925a 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 # -# 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 @@ -22,25 +20,23 @@ jobs: 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 + # install dependencies via mise - name: Dependencies - run: | - sudo gem install bundler:2.6.3 - bundle install + run: mise install # run Danger - name: Run Danger - run: bundle exec danger --fail-on-errors=true + run: mise exec -- danger --fail-on-errors=true env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..fe88925 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +"gem:danger" = "8.2.3" diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 8ce1e30..0000000 --- a/Gemfile +++ /dev/null @@ -1,7 +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' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 6beb88a..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,86 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - addressable (2.8.8) - public_suffix (>= 2.0.2, < 8.0) - claide (1.1.0) - claide-plugins (0.9.2) - cork - nap - open4 (~> 1.3) - colored2 (3.1.2) - 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) - 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-em_http (1.0.0) - faraday-em_synchrony (1.0.1) - faraday-excon (1.1.0) - faraday-http-cache (2.6.1) - faraday (>= 0.8) - faraday-httpclient (1.0.1) - faraday-multipart (1.2.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) - git (1.19.1) - addressable (~> 2.8) - rchardet (~> 1.8) - kramdown (2.5.2) - rexml (>= 3.4.4) - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - multipart-post (2.4.1) - nap (1.1.0) - no_proxy_fix (0.1.2) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - open4 (1.3.4) - public_suffix (7.0.2) - rchardet (1.10.0) - rexml (3.4.4) - ruby2_keywords (0.0.5) - sawyer (0.9.3) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.6.0) - -PLATFORMS - arm64-darwin-24 - ruby - -DEPENDENCIES - bundler (~> 2.6.3) - danger (~> 8.2.0) - -BUNDLED WITH - 2.6.3 From da1e5ad9d734db589ffe18209c4a3291b2c6dad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:33:53 +0100 Subject: [PATCH 30/52] feat: workflow fix --- .github/workflows/integrations.yml | 45 ----------- .github/workflows/pr-integrations.yml | 106 ++++++++++++++++++++++++++ .gitignore | 3 + 3 files changed, 109 insertions(+), 45 deletions(-) delete mode 100644 .github/workflows/integrations.yml create mode 100644 .github/workflows/pr-integrations.yml diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml deleted file mode 100644 index ff7925a..0000000 --- a/.github/workflows/integrations.yml +++ /dev/null @@ -1,45 +0,0 @@ -### Integrations workflow ### -# -# Runs Danger checks, Swiftlint and tests. -# -# github.token - is automatically assigned to your workflow by Github -# - -name: Integrations - -# - by default this workflow is triggered on every change in a pull request - this can have cost implications when running on macos-latest -# - read about other possible triggers on https://help.github.com/en/actions/reference/events-that-trigger-workflows -on: - pull_request: - types: [opened, edited, reopened, synchronize] - -jobs: - integrations: - # runs-on: macos-latest # use this for repos outside of the STRV Github org - 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 - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.4.1 - with: - access_token: ${{ github.token }} - - # checkout the project repository - - name: Checkout - uses: actions/checkout@v1 - - # install dependencies via mise - - name: Dependencies - run: mise install - - # run Danger - - name: Run Danger - run: mise exec -- danger --fail-on-errors=true - env: - GITHUB_TOKEN: ${{ github.token }} - - # run tests - - name: Run tests - run: swift test --enable-code-coverage diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml new file mode 100644 index 0000000..f124be7 --- /dev/null +++ b/.github/workflows/pr-integrations.yml @@ -0,0 +1,106 @@ +### PR Integrations Workflow ### +# +# This workflow runs on pull requests and performs: +# - Danger checks +# - Unit Tests +# +# Authentication: +# - `github.token` is automatically provided by GitHub. +# +# Notes: +# - To enable Fastlane tests, ensure `CommonFastfile` is imported in your Fastfile +# from the shared ios-fastlane repository. +# - By default, this workflow runs on every PR change. Be mindful of costs when +# using macos-latest. It can also be triggered manually in GitHub. +# - See more trigger options: https://help.github.com/en/actions/reference/events-that-trigger-workflows +# +# File: .github/workflows/pr-integrations.yml + +name: Integrations (PR) + +on: + workflow_dispatch: + pull_request: + types: [opened, edited, reopened, synchronize, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Common environment variables available to all jobs +env: + WORKSPACE: STRVTemplate.xcworkspace + TEST_SCHEME: STRVTemplate + DEVICES: "iPhone 13 Pro" + +jobs: + # Quality Checks (Danger) Job + quality-checks: + name: Quality Checks + runs-on: [self-hosted, macOS, mobile-ci] + if: github.event.pull_request.draft == false + timeout-minutes: 20 + steps: + # Checkout the Project Repository + - name: Checkout + uses: actions/checkout@v4 + + # Istall Mise (Dev env setup tool) + - name: Install Mise + uses: jdx/mise-action@v2 + with: + install: true + cache: false # change to true when running on macos-latest instead of self-hosted + experimental: true + + # Cache Dependencies + - name: Cache Dependencies + uses: ./.github/actions/cache-dependencies + + # Install dependencies (run install script) + - name: Install Dependencies + run: ./setup.swift install + + # uncomment if you are using SwiftLint via SPM plugin + # Set SwiftLint version for Danger + # - name: Set SwiftLint version + # run: echo "SWIFTLINT_VERSION=$(cat ./.swiftlint-version)" >> $GITHUB_ENV + + # Run Danger + - name: Run Danger + run: bundle exec danger --fail-on-errors=true + env: + GITHUB_TOKEN: ${{ github.token }} + + # Unit Tests Job + unit-tests: + name: Unit Tests + runs-on: [self-hosted, macOS, mobile-ci] + if: github.event.pull_request.draft == false + timeout-minutes: 20 + steps: + # Checkout the Project Repository + - name: Checkout + uses: actions/checkout@v4 + + # Istall Mise (Dev env setup tool) + - name: Install Mise + uses: jdx/mise-action@v2 + with: + install: true + cache: false # change to true when running on macos-latest instead of self-hosted + experimental: true + + # Cache dependencies + - name: Cache Dependencies + uses: ./.github/actions/cache-dependencies + + # Install dependencies (run install script) + - name: Install Dependencies + run: ./setup.swift install + + # Run Unit Tests + - name: Run Unit Tests + run: bundle exec fastlane tests without_dependencies:true + env: + TEST_PLAN: UnitTestPlan 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 From ecdf20ad0ded9fbb6bb1578eaf87f78d9dea41a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:40:15 +0100 Subject: [PATCH 31/52] fix: mise.toml --- .mise.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.mise.toml b/.mise.toml index fe88925..ccb587f 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,2 +1,8 @@ [tools] -"gem:danger" = "8.2.3" +ruby = "3.4.6" +swiftlint = "0.57.0" +swiftformat = "0.54.5" + +[settings] +legacy_version_file = false +experimental = true From 4480b0633154acb1cfc1d791793149ce96d3a9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:46:12 +0100 Subject: [PATCH 32/52] fix: github setup --- .github/actions/cache-dependencies/action.yml | 14 ++++++++++++++ .github/config/.swiftlint.yml | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 .github/actions/cache-dependencies/action.yml create mode 100644 .github/config/.swiftlint.yml diff --git a/.github/actions/cache-dependencies/action.yml b/.github/actions/cache-dependencies/action.yml new file mode 100644 index 0000000..bc731c0 --- /dev/null +++ b/.github/actions/cache-dependencies/action.yml @@ -0,0 +1,14 @@ +name: Create iOS simulator +description: Create simulator to be used for a test run +runs: + using: composite + steps: + - name: Gem Cache + uses: actions/cache@v4 + id: dependencies_cache + with: + path: | + vendor + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- 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 From 75dc27e13d19ff49c3736bd592b6ac425b04af10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:47:39 +0100 Subject: [PATCH 33/52] fix: setup.swift --- setup.swift | 762 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 762 insertions(+) create mode 100755 setup.swift diff --git a/setup.swift b/setup.swift new file mode 100755 index 0000000..987ed44 --- /dev/null +++ b/setup.swift @@ -0,0 +1,762 @@ +#!/usr/bin/env swift +import Foundation + +// swiftlint:disable all + +let shell = Shell() +// This enables the script to exit when the user presses Ctrl+C +signal(SIGINT) { _ in + Task { @MainActor in + shell.terminateCurrentTask() + exit(0) + } +} + +enum Command: String, CaseIterable { + case help + case setup + case install + case installBundle = "install-bundle" + case installDependencies = "install-dependencies" + case updateDependencies = "update-dependencies" + case setupTuist = "setup-tuist" + case setupHooks = "setup-hooks" + case renameProject = "rename-project" + case setupCI = "setup-ci" + case removeTuistFiles = "remove-tuist-files" + case setupVersionIcon = "install-version-icon" +} + +extension Command { + var description: String { + switch self { + case .help: + "Prints this manual." + + case .setup: + "Runs the following commands: setupCI -> renameProject -> setupHooks -> installMiseDependencies -> installBundle -> setupTuist (if needed) -> setupVersionIcon." + + case .install: + "Runs the following commands: setupHooks -> installMiseDependencies -> installBundle -> installTuistDependencies -> installVersionIcon (if needed)." + + case .installBundle: + "We use Gemfile to ensure that you have all dependencies in correct versions. `install-bundle` installs all dependencies to the project directory." + + case .installDependencies: + "Installs Mise and Tuist dependencies (Tuist, SwiftLint, SwiftFormat) if needed." + + case .updateDependencies: + "Updates dependencies (Tuist, SwiftLint, SwiftFormat) to the latest versions" + + case .setupTuist: + "Installs Tuist environment, installs dependencies and generates the project files, adds appropriate files to the gitignore, removes gitignored files from git and setups git hooks." + + case .setupHooks: + "Creates symbolic links for hooks in githooks to the .git/hooks dictionary." + + case .renameProject: + "If you want to rename the project from `STRVTemplate` to something different (and you probably want that), just run the following command. You'll be asked for the name." + + case .setupCI: + "Copies .env and workflow files as needed." + + case .removeTuistFiles: + "Removes Tuist definition files." + + case .setupVersionIcon: + "VersionIcon is optional feature. It creates an app icon overlay with the ribbon and/or version (build) text based on the app configuration. VersionIcon is added as script build phase. It is STRV internal project and it is highly customizable." + } + } +} + +enum SetupError: LocalizedError { + case fileOperationFailed(String? = nil) + case emptyProjectName + case missingWorkspace + case miseNotInstalled(command: String) + case miseNotActivated(command: String) + + var errorDescription: String? { + switch self { + case let .fileOperationFailed(error?): + "file operation failed: \(error)" + case .fileOperationFailed(.none): + "file operation failed" + case .emptyProjectName: + "project name can't be empty" + case .missingWorkspace: + "no workspace in current directory" + case let .miseNotInstalled(command): + "Please install Mise (https://mise.jdx.dev/getting-started.html#_1-install-mise-cli) and run the `\(command)` command again." + case let .miseNotActivated(command): + "Please activate Mise (https://mise.jdx.dev/getting-started.html#_2a-activate-mise) and run the `\(command)` command again." + } + } +} + +@MainActor +final class Main { + private let commandRunner = CommandRunner() + + func run() async { + let argumentCommand = CommandLine.argc > 1 ? CommandLine.arguments[1] : nil + + if let argumentCommand { + if let command = Command(rawValue: argumentCommand) { + await commandRunner.run(command) + } else { + print("⚠️ Error: unknown command") + exit(1) + } + } else { + await askForCommand() + } + } +} + +private extension Main { + func askForCommand() async { + printCommands() + print("\n") + if let commandNumber = askForCommandNumber(), + Command.allCases.indices.contains(commandNumber) + { + let command = Command.allCases[commandNumber] + print(" Next time you can do this by directly typing `./setup.swift \(command.rawValue)`") + await commandRunner.run(command) + } else { + print("⚠️ Error: unknown command") + exit(1) + } + } + + func printCommands() { + for (index, command) in Command.allCases.enumerated() { + print("\(index): \(command.rawValue)") + } + } + + func askForCommandNumber() -> Int? { + print("\n💬 Which number would you like to run?") + return readLine().map(Int.init) ?? nil + } +} + +@MainActor +final class CommandRunner { + func run(_ command: Command) async { + do { + switch command { + case .help: + printManual() + + case .installBundle: + try await installBundle(shouldVerifyMise: true) + + case .installDependencies: + let tuistDefinitionFileExists = await tuistDefinitionFileExists() + try await installMiseDependencies(shouldVerifyMise: true) + try await installTuistDependencies(isUsingTuist: tuistDefinitionFileExists) + + case .updateDependencies: + await updateDependencies() + + case .setupTuist: + try await verifyMiseIsInstalled(command: command) + try await setupTuist() + + case .setupHooks: + try setupHooks() + + case .renameProject: + try await renameProject() + + case .setupCI: + await setupCI() + + case .setup: + try await setup() + + case .install: + try await install() + + case .removeTuistFiles: + await removeTuistFiles() + + case .setupVersionIcon: + let tuistDefinitionFileExists = await tuistDefinitionFileExists() + if tuistDefinitionFileExists { + try? await setupVersionIcon(projectName: nil) + try await installTuistDependencies(isUsingTuist: tuistDefinitionFileExists) + } else { + print("⚠️ Error: VersionIcon requires Tuist and it is not installed. Please run `./setup.swift setup-tuist` first.") + exit(1) + } + } + } catch { + print("⚠️ Error: \(error.localizedDescription)") + exit(1) + } + } +} + +private extension CommandRunner { + var isCI: Bool { + guard let ciValue = ProcessInfo.processInfo.environment["CI"] else { + return false + } + return ciValue == "true" || ciValue == "1" + } + + var homeDirectory: String { + FileManager.default.homeDirectoryForCurrentUser.path + } + + func printManual() { + print("\n🔵 Available commands:\n") + for (index, command) in Command.allCases.enumerated() { + print("\(index): \(command.rawValue)") + print("// \(command.description)\n") + } + } + + func setup() async throws { + try await verifyMiseIsInstalled(command: .setup) + await setupCI(offerSkip: true) + let projectName = try await renameProject() + try setupHooks() + try await installMiseDependencies(shouldVerifyMise: false) + try await installBundle(shouldVerifyMise: false) + try await setupVersionIcon(projectName: projectName) + try await setupTuist() + } + + func install() async throws { + try await verifyMiseIsInstalled(command: .install) + try setupHooks() + let tuistDefinitionFileExists = await tuistDefinitionFileExists() + try await installMiseDependencies(shouldVerifyMise: false) + try await installBundle(shouldVerifyMise: false) + try await installTuistDependencies(isUsingTuist: tuistDefinitionFileExists) + try installVersionIconIfNeeded() + } + + func installBundle(shouldVerifyMise: Bool) async throws { + if shouldVerifyMise { + try await verifyMiseIsInstalled(command: .installBundle) + } + + print("🔵 Install bundle: \(bundlerVersion)") + await shell.runAndExitIfFailure("./scripts/setup_bundler.sh") + await shell.runAndExitIfFailure("bundle _\(bundlerVersion)_ install") + } + + func installMiseDependencies(shouldVerifyMise: Bool) async throws { + if shouldVerifyMise { + try await verifyMiseIsInstalled(command: .installDependencies) + } + + print("🔵 Install Mise dependencies") + + await shell.runAndExitIfFailure("mise install -y") + } + + func installTuistDependencies(isUsingTuist: Bool) async throws { + print("🔵 Install Tuist dependencies") + + if isUsingTuist { + // It seems we need to use `mise exec` because after the initial Mise installation the $PATH doesn't correctly update + // not even via calling `eval "$(mise activate zsh --shims)"` so Mise is unable to find the correct tool (in this case tuist) + // Every other setup run works correctly without calling `mise exec` because the $PATH updating is the working fine. + // But for the initial "edge-case" it seems we need to keep the `mise exec`. + await shell.runAndExitIfFailure("mise exec -- tuist install") + await shell.runAndExitIfFailure("mise exec -- tuist generate --no-open") + } + } + + func updateDependencies() async { + await shell.runAndExitIfFailure("ruby scripts/update_dependencies.rb") + } + + func setupTuist() async throws { + print("🔵 Setup Tuist") + guard await tuistDefinitionFileExists() else { + print("Tuist Project.swift definition file is missing.") + return + } + try await installTuistDependencies(isUsingTuist: true) + + await shell.runAndExitIfFailure("./scripts/setup_tuist_githook.sh") + try setupHooks() + } + + func setupHooks() throws { + guard !isCI else { + return + } + + print("🔵 Setup hooks") + try installHooks() + } + + func installHooks() throws { + let fileManager = FileManager.default + let currentDirectoryPath = fileManager.currentDirectoryPath + let hooksDirectoryPath = currentDirectoryPath + "/.git/hooks" + let sourceDirectoryPath = currentDirectoryPath + "/.githooks" + + if !fileManager.fileExists(atPath: hooksDirectoryPath) { + print("🟡 Creating missing .git/hooks/ directory") + try fileManager.createDirectory(atPath: hooksDirectoryPath, withIntermediateDirectories: true, attributes: nil) + } + + let hooks = try fileManager.contentsOfDirectory(atPath: sourceDirectoryPath) + .filter { !$0.hasPrefix(".") && URL(fileURLWithPath: $0).pathExtension.isEmpty } + + for hook in hooks { + let target = currentDirectoryPath + "/.git/hooks/\(hook)" + let source = currentDirectoryPath + "/.githooks/\(hook)" + let backup = "\(target).bak" + + guard fileManager.fileExists(atPath: source) else { + print("🟡 Skipping \(hook) hook because it doesn't exist at expected path: \(source)") + continue + } + + if + let destination = try? fileManager.destinationOfSymbolicLink(atPath: target), + destination == source + { + print("🟢 \(hook) hook is already installed") + continue + } + + if fileManager.contentsEqual(atPath: target, andPath: source) { + print("🟢 \(hook) hook is already installed") + continue + } + + if fileManager.fileExists(atPath: target) { + print("🟡 Found an existing hook for \(hook). Creating a backup at \(backup)") + do { + if fileManager.fileExists(atPath: backup) { + try fileManager.removeItem(atPath: backup) + } + try fileManager.moveItem(atPath: target, toPath: backup) + } catch { + throw SetupError.fileOperationFailed("Backup creation failed: \(error.localizedDescription)") + } + } + + do { + try fileManager.createSymbolicLink(atPath: target, withDestinationPath: source) + } catch { + throw SetupError.fileOperationFailed("\(hook) Symlink creation failed: \(error.localizedDescription)") + } + print("🟢 Installed \(hook) hook") + } + } + + @discardableResult + func renameProject() async throws -> String { + print("\n🔵 Rename project") + print("💬 New project name:") + guard let projectName = readLine(), !projectName.isEmpty else { + throw SetupError.emptyProjectName + } + print("🔵 Renaming project... (can take a minute)") + await shell.runAndExitIfFailure("./scripts/rename.sh \"\(projectName)\"") + print("🟢 Renamed to \(projectName)") + return projectName + } + + func setupCI(offerSkip: Bool = false) async { + print("🔵 Setup CI") + + print("🔵 Setting up default Integrations workflow") + await shell.runAndExitIfFailure("./scripts/setup_ci.sh integrations") + + let availableWorkflows = [ + "testflight", + "enterprise", + ] + let workflowsString = availableWorkflows + .enumerated() + .map { "\($0): \($1)" } + .joined(separator: " | ") + print("\n🔵 Available CI workflows: \(workflowsString)") + print("💬 Which workflow number would you like to select?" + (offerSkip ? " Just press enter to skip." : "")) + if let workflowNumber = readLine().map(Int.init) ?? nil, + availableWorkflows.indices.contains(workflowNumber) + { + let selectedWorkflow = availableWorkflows[workflowNumber] + await shell.runAndExitIfFailure("./scripts/setup_ci.sh \"\(selectedWorkflow)\"") + } else { + print("🔵 Skipped") + } + } + + func removeTuistFiles() async { + print("🔵 Remove Tuist files") + await shell.runAndExitIfFailure("rm -rf Tuist App/Project.swift Workspace.swift .tuist-version .githooks/post-checkout .package.resolved Derived") + } + + func tuistDefinitionFileExists() async -> Bool { + await shell.run("test -e App/Project.swift") == 0 + } + + func shouldSetupVersionIcon() -> Bool { + print("\n💬 Do you want to setup VersionIcon script (https://github.com/strvcom/ios-version-icon) ? (y/n)") + let answer = readLine() + if answer == "y" { + return true + } else if answer == "n" { + return false + } else { + print("⚠️ Unknown answer") + exit(1) + } + } + + func setupVersionIcon(projectName: String?) async throws { + // Test whether Tuist is installed + var isdirectory : ObjCBool = true + guard FileManager.default.fileExists(atPath: "Tuist", isDirectory: &isdirectory) else { + print("⚠️ Tuist is not installed") + return + } + + guard shouldSetupVersionIcon() else { + return + } + + var usedProjectName: String + if let projectName { + usedProjectName = projectName + } else { + let currentDirectory = FileManager.default.currentDirectoryPath + let enumerator = FileManager.default.enumerator(atPath: currentDirectory) + let filePaths = enumerator?.allObjects as! [String] + let workspaces = filePaths.filter { $0.contains(".xcworkspace") } + + usedProjectName = try getProjectName(from: workspaces) + } + try setupVersionIconGlobalConstants() + try setupVersionIconUpdateGitIgnore(projectName: usedProjectName) + await setupVersionIconRemoveIconsFromGit(projectName: usedProjectName) + } + + /// Gets the name for project - from the workspace file or by asking the user + func getProjectName(from workspaces: [String]) throws -> String { + // Handle the case that there are many workspace files in the folder + if workspaces.count > 1 { + // Ask the user + print("\n💬 Project name: ") + guard let projectName = readLine(), !projectName.isEmpty else { + throw SetupError.emptyProjectName + } + return projectName + } + // Otherwise derive the project name from single workspace file + else { + guard let workspace = workspaces.first else { + throw SetupError.missingWorkspace + } + + return workspace.replacingOccurrences(of: ".xcworkspace", with: "") + } + } + + /// Change Tuist GlobalConstants.swift + func setupVersionIconGlobalConstants() throws { + print("🔵 Modifying Tuist GlobalConstants") + var globalConstantsContents = try String(contentsOfFile: globalConstantsPath, encoding: .utf8) + globalConstantsContents = globalConstantsContents.replacing("useVersionIcon = false", with: "useVersionIcon = true") + try globalConstantsContents.write(toFile: globalConstantsPath, atomically: true, encoding: .utf8) + } + + /// Update .gitignore for VersionIcon + func setupVersionIconUpdateGitIgnore(projectName: String) throws { + print("🔵 Updating .gitignore") + let gitIgnore = ".gitignore" + var gitIgnoreContents = try String(contentsOfFile: gitIgnore) + + if gitIgnoreContents.contains("AppIcon.appiconset") { + print("VersionIcon is already set in .gitignore") + } else { + gitIgnoreContents.append( + """ + + # VersionIcon + + App/{{AppName}}/Application/Resources/Assets.xcassets/AppIcon.appiconset/* + """ + .replacing("{{AppName}}", with: projectName) + ) + try gitIgnoreContents.write(toFile: gitIgnore, atomically: true, encoding: .utf8) + } + } + + /// Remove icon files from git. We want to avoid commiting the changed icons with every commit. + /// AppIcon is now cosidered as only local and AppIconOriginal is shared image source. + func setupVersionIconRemoveIconsFromGit(projectName: String) async { + print("🔵 Removing AppIcon images from git") + await shell.runAndExitIfFailure("git rm -f \"App/\(projectName)/Application/Resources/Assets.xcassets/AppIcon.appiconset/*\"") + } + + /// Installs VersionIcon if needed. + /// `AppIcon` is git ignored, but Xcode still needs it to build the app. + /// We copy the original icon to `AppIcon.appiconset` if it doesn't exist. + /// This usually happens when existing project is cloned and the icon is not present. + func installVersionIconIfNeeded() throws { + guard try isVersionIconSetup() else { + print("🔵 VersionIcon is not set up. Skipping.") + return + } + + guard let projectName = try getGlobalConstantsProjectName() else { + print("🔴 Could not determine project name from GlobalConstants.swift. Please ensure it is set correctly.") + return + } + let originalAppIconPath = "App/\(projectName)/Application/Resources/Assets.xcassets/AppIconOriginal.appiconset" + let appIconPath = "App/\(projectName)/Application/Resources/Assets.xcassets/AppIcon.appiconset" + + guard FileManager.default.fileExists(atPath: originalAppIconPath) else { + print("🔴 Original AppIcon not found at \(originalAppIconPath). Please ensure it exists.") + return + } + + guard !FileManager.default.fileExists(atPath: appIconPath) else { + print("🟢 AppIcon is already set up.") + return + } + + try FileManager.default.copyItem(atPath: originalAppIconPath, toPath: appIconPath) + print("🟢 Copied original AppIcon to \(appIconPath)") + } + + func isVersionIconSetup() throws -> Bool { + let contents = try String(contentsOfFile: globalConstantsPath, encoding: .utf8) + return contents.contains("useVersionIcon = true") + } + + var bundlerVersion: String { + let fileName = ".bundler-version" + let fileURL = URL(filePath: fileName) + do { + return try String(contentsOf: fileURL, encoding: .utf8) + } catch { + print("⚠️ Error: \(String(describing: error))") + exit(1) + } + } + + func getGlobalConstantsProjectName() throws -> String? { + let globalConstantsContents = try String(contentsOfFile: globalConstantsPath, encoding: .utf8) + let pattern = #".*projectName\s*=\s*"([^"]+)""# + let regex = try NSRegularExpression(pattern: pattern, options: []) + + if let match = regex.firstMatch(in: globalConstantsContents, options: [], range: NSRange(globalConstantsContents.startIndex..., in: globalConstantsContents)), + let range = Range(match.range(at: 1), in: globalConstantsContents) + { + let projectName = String(globalConstantsContents[range]) + return projectName + } + + return nil + } + + var globalConstantsPath: String { + "./Tuist/ProjectDescriptionHelpers/GlobalConstants.swift" + } + + func promptWithDefault(message: String, defaultValue: String = "y") -> String { + print("\(message) [Y/n]: ", terminator: "") + + let response = readLine()?.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + if let response, !response.isEmpty { + return response + } + return defaultValue + } +} + +// MARK: - Mise setup + +private extension CommandRunner { + /// Checks if Mise is installed, verifies if activated correctly, and handles installation + activation if necessary. + func verifyMiseIsInstalled(command: Command) async throws { + // If Mise is installed + if await shell.run("which -s mise", printOutput: false) == 0 { + print("🔵 Mise is installed.") + } else { + // Check whether it is installed in the expected path + if FileManager.default.fileExists(atPath: "\(homeDirectory)/.local/bin/mise") { + print("🟡 Mise is installed, but not activated.") + let isZshrcUpdated = await verifyAndUpdateZshrc(command: command) + if !isZshrcUpdated { + throw SetupError.miseNotActivated(command: command.rawValue) + } else { + // Force the terminal restart + exit(0) + } + } else { + let install = promptWithDefault(message: "🔴 Mise is not installed and it's required. Would you like to install it now?") + if install == "y" { + try await installMise(command: command) + } else { + throw SetupError.miseNotInstalled(command: command.rawValue) + } + } + } + } + + /// Installs Mise using a shell script and updates .zshrc to activate it. + func installMise(command: Command) async throws { + print("🔵 Installing Mise...") + await shell.runAndExitIfFailure("curl https://mise.jdx.dev/install.sh | sh", printOutput: false) + _ = await verifyAndUpdateZshrc(command: command) + exit(0) + } + + /// Verifies if the Mise activation command is present in .zshrc and prompts to add it if missing. + /// Returns: True/false whether the zshrc was successfully updated + func verifyAndUpdateZshrc(command: Command) async -> Bool { + let zshrcPath = "\(homeDirectory)/.zshrc" + let activationCommand = "eval \"$(\(homeDirectory)/.local/bin/mise activate zsh)\"" + + do { + let zshrcContents = try String(contentsOfFile: zshrcPath, encoding: .utf8) + + if !zshrcContents.contains(activationCommand) { + let response = promptWithDefault(message: "🔴 Mise activation command is not in `~/.zshrc`. Would you like to add it now?") + if response == "y" { + appendToZshrc(command: activationCommand) + print("🟡 In order to finish the Mise install, please restart your Terminal and then run `./setup.swift \(command.rawValue)` again.") + return true + } else { + printManualZshrcUpdateInstructions(command: command) + return false + } + } else { + if !isPathUpdated() { + print("🟡 Mise is activated in ~/.zshrc, but the current Terminal session does not recognize it. Please restart your Terminal and then run `./setup.swift \(command.rawValue)` again.") + exit(0) + } + } + } catch { + print("Failed to read ~/.zshrc: \(error)") + } + return true + } + + func printManualZshrcUpdateInstructions(command: Command) { + print("Please manually add the following line to your .zshrc to activate Mise:") + print("eval \"$(\(homeDirectory)/.local/bin/mise activate zsh)\"") + print("After updating, please restart your Terminal and run the run `./setup.swift \(command.rawValue)` again.") + } + + /// Appends a given command to the user's `.zshrc` file. + func appendToZshrc(command: String) { + let zshrcPath = "\(homeDirectory)/.zshrc" + let fullCommand = "\n\n# Mise \n\(command)\n" + + do { + // Check if .zshrc exists and append the command + if FileManager.default.fileExists(atPath: zshrcPath) { + let fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: zshrcPath)) + fileHandle.seekToEndOfFile() + if let data = fullCommand.data(using: .utf8) { + fileHandle.write(data) + fileHandle.closeFile() + print("🟢 ~/.zshrc was adjusted successfully.") + } + } else { + // If .zshrc does not exist, create it and write the command + try fullCommand.write(to: URL(fileURLWithPath: zshrcPath), atomically: true, encoding: .utf8) + print(".zshrc file created and command added.") + } + } catch { + print("An error occurred: \(error)") + } + } + + func isPathUpdated() -> Bool { + let misePath = "\(homeDirectory)/.local/bin" + let pathEnvironmentVariable = ProcessInfo.processInfo.environment["PATH"] ?? "" + return pathEnvironmentVariable.split(separator: ":").contains { String($0) == misePath } + } +} + +@MainActor +final class Shell { + private var currentTask: Process? + + // credit: https://stackoverflow.com/a/38088651 + // Shell with continuous printing to the console + // - Returns: Shell exit code + @discardableResult + func run(_ command: String, printOutput: Bool = true) async -> Int32 { + await withCheckedContinuation { continuation in + let task = Process() + currentTask = task + + let outpipe = Pipe() + task.standardOutput = outpipe + task.arguments = ["-c", command] + task.executableURL = URL(fileURLWithPath: "/bin/zsh") + task.standardInput = nil + + task.terminationHandler = { result in + DispatchQueue.main.async { + if self.currentTask === task { + self.currentTask = nil + } + continuation.resume(returning: result.terminationStatus) + } + } + + let outputHandle = outpipe.fileHandleForReading + outputHandle.readabilityHandler = { pipe in + guard printOutput else { return } + if let line = String(data: pipe.availableData, encoding: .utf8) { + print(line, terminator: "") + } else { + print("⚠️ Error decoding data: \(pipe.availableData)") + } + } + + try? task.run() + } + } + + func terminateCurrentTask() { + guard let task = currentTask else { + return + } + task.terminate() + } + + func runAndExitIfFailure(_ command: String, printOutput: Bool = true) async { + let exitCode = await run(command, printOutput: printOutput) + if exitCode != 0 { + print("❌ Command failed with exit code \(exitCode)") + exit(1) + } + } + + /// Shell with printing to the console when command terminates. + /// Allowing prompt asking for the user password. + func runWithPrivileges(_ command: String) async { + let myAppleScript = "do shell script \"\(command)\"" + var error: NSDictionary? + let scriptObject = NSAppleScript(source: myAppleScript)! + let output = scriptObject.executeAndReturnError(&error) + let ouputWithReplacedNewLines = (output.stringValue ?? "").replacingOccurrences(of: "\r\n", with: "\n") + print(ouputWithReplacedNewLines) + } +} + +await Main().run() + +// swiftlint:enable all From c356e07cce70b609d36854091c1e36fff232c4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:49:32 +0100 Subject: [PATCH 34/52] fix: bundler version --- .bundler-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .bundler-version diff --git a/.bundler-version b/.bundler-version new file mode 100644 index 0000000..6ab1bff --- /dev/null +++ b/.bundler-version @@ -0,0 +1 @@ +2.5.17 \ No newline at end of file From a3028dbeb58fa96c78a1eda2da5329cf5b23cbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:52:56 +0100 Subject: [PATCH 35/52] feat: scripts --- scripts/ModuleGenerator.swift | 404 +++++++++++++++++++++++++++++++++ scripts/git-format-staged | 282 +++++++++++++++++++++++ scripts/rename.sh | 40 ++++ scripts/setup_bundler.sh | 11 + scripts/setup_ci.sh | 30 +++ scripts/setup_tuist_githook.sh | 23 ++ scripts/swiftformat.sh | 43 ++++ scripts/swiftlint.sh | 41 ++++ scripts/update_dependencies.rb | 128 +++++++++++ scripts/verify_ruby.sh | 105 +++++++++ 10 files changed, 1107 insertions(+) create mode 100755 scripts/ModuleGenerator.swift create mode 100755 scripts/git-format-staged create mode 100755 scripts/rename.sh create mode 100755 scripts/setup_bundler.sh create mode 100755 scripts/setup_ci.sh create mode 100755 scripts/setup_tuist_githook.sh create mode 100755 scripts/swiftformat.sh create mode 100755 scripts/swiftlint.sh create mode 100644 scripts/update_dependencies.rb create mode 100755 scripts/verify_ruby.sh diff --git a/scripts/ModuleGenerator.swift b/scripts/ModuleGenerator.swift new file mode 100755 index 0000000..95d2153 --- /dev/null +++ b/scripts/ModuleGenerator.swift @@ -0,0 +1,404 @@ +#!/usr/bin/swift +import Foundation + +// swiftlint:disable disable_print + +// This enables the script to exit when the user presses Ctrl+C +signal(SIGINT) { _ in exit(0) } + +// MARK: - Enums + +enum LayerType: String, CaseIterable { + case feature = "Feature" + case service = "Service" + case library = "Library" +} + +enum ModuleGenerationError: Error { + case failedToReadDependencyFile + case failedToWriteDependencyFile + case invalidInsertionPoint + case failedToReadTuistOutput +} + +// MARK: - Structs + +struct ModuleInfo { + let layer: LayerType + let moduleName: String + let hasInterface: Bool + let hasTestResources: Bool + let hasUnitTests: Bool + let hasUITests: Bool + let hasExample: Bool + let hasLocalizedStrings: Bool + let hasAssets: Bool +} + +// MARK: - Module Generator + +/// This class utilizes Tuist's `scaffold` command to generate the basic structure and files +/// for new modules in a modularized project. It automates the process of creating modules, +/// their interfaces, tests, and example targets based on provided specifications. +final class ModuleGenerator { + private let fileManager = FileManager.default + private let currentPath = "./" + + func generateModule() { + do { + let moduleInfo = getModuleInfo() + try scaffoldModule(moduleInfo) + try registerDependency(moduleInfo) + + let createdTargets = listCreatedTargets(moduleInfo) + print("✅ Module '\(moduleInfo.moduleName)' created successfully in the \(moduleInfo.layer.rawValue) layer!") + print(" Created targets: \(createdTargets)") + } catch { + print("❌ Error creating module: \(error)") + } + } +} + +private extension ModuleGenerator { + func getModuleInfo() -> ModuleInfo { + print("\u{001B}[1mWhat's the layer of this module?\u{001B}[0m") + for (index, layer) in LayerType.allCases.enumerated() { + print("\(index + 1). \(layer.rawValue)") + } + + let layer = LayerType.allCases[getIntInput(min: 1, max: LayerType.allCases.count) - 1] + + print("\u{001B}[1mEnter module name:\u{001B}[0m", terminator: " ") + let moduleName = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + let hasResources = getBoolInput("Do you want to include resources (strings or assets) in this module?") + var hasLocalizedStrings = false + var hasAssets = false + + if hasResources { + hasLocalizedStrings = getBoolInput("Do you want to include localized strings?") + hasAssets = getBoolInput("Do you want to include an Asset catalog (for colors and images)?") + } + + let hasInterface = getBoolInput("Do you want to include an Interface target?") + let hasUnitTests = getBoolInput("Do you want to include unit tests target?") + let hasUITests = getBoolInput("Do you want to include UI tests target?") + let hasTestResources = if hasUnitTests || hasUITests { + getBoolInput("Do you need a TestResources target for test utilities and mocks?") + } else { + false + } + + let hasExample = hasUITests || getBoolInput("Do you want to create an Example target (for a demo app)?") + + print("\u{001B}[1m⏳ Generating...:\u{001B}[0m\n\n", terminator: " ") + + return ModuleInfo( + layer: layer, + moduleName: moduleName, + hasInterface: hasInterface, + hasTestResources: hasTestResources, + hasUnitTests: hasUnitTests, + hasUITests: hasUITests, + hasExample: hasExample, + hasLocalizedStrings: hasLocalizedStrings, + hasAssets: hasAssets + ) + } + + func scaffoldModule(_ moduleInfo: ModuleInfo) throws { + try scaffoldMainModule(moduleInfo) + + if moduleInfo.hasInterface { try scaffoldInterface(moduleInfo) } + if moduleInfo.hasTestResources { try scaffoldTestResources(moduleInfo) } + if moduleInfo.hasUnitTests { try scaffoldTests(moduleInfo) } + if moduleInfo.hasUITests { try scaffoldUITests(moduleInfo) } + if moduleInfo.hasExample { try scaffoldExample(moduleInfo) } + if moduleInfo.hasLocalizedStrings { try scaffoldLocalizedStrings(moduleInfo) } + if moduleInfo.hasAssets { try scaffoldAssets(moduleInfo) } + } + + func scaffoldMainModule(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "Module", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + "--interface", moduleInfo.hasInterface.description, + "--testresources", moduleInfo.hasTestResources.description, + "--tests", moduleInfo.hasUnitTests.description, + "--uitests", moduleInfo.hasUITests.description, + "--example", moduleInfo.hasExample.description, + "--localizedstrings", moduleInfo.hasLocalizedStrings.description, + "--assets", moduleInfo.hasAssets.description, + ] + + try runTuistCommand(args) + } + + func scaffoldInterface(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "Interface", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + ] + + try runTuistCommand(args) + } + + func scaffoldTestResources(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "TestResources", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + ] + + try runTuistCommand(args) + } + + func scaffoldTests(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "Tests", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + ] + + try runTuistCommand(args) + } + + func scaffoldUITests(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "UITests", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + ] + + try runTuistCommand(args) + } + + func scaffoldExample(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "Example", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + ] + + try runTuistCommand(args) + } + + func scaffoldLocalizedStrings(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "Localizable", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + ] + + try runTuistCommand(args) + } + + func scaffoldAssets(_ moduleInfo: ModuleInfo) throws { + let args = [ + "scaffold", + "Assets", + "--layer", moduleInfo.layer.rawValue, + "--name", moduleInfo.moduleName, + ] + + try runTuistCommand(args) + } + + func registerDependency(_ moduleInfo: ModuleInfo) throws { + let dependencyFilePath = "\(currentPath)Tuist/ProjectDescriptionHelpers/Dependencies/Dependency+Module.swift" + + guard var content = try? String(contentsOfFile: dependencyFilePath, encoding: .utf8) else { + throw ModuleGenerationError.failedToReadDependencyFile + } + + let newDependencies = createDependencyDeclarations(for: moduleInfo) + + guard let range = content.range(of: "public extension TargetDependency.Module.\(moduleInfo.layer.rawValue) {") else { + throw ModuleGenerationError.invalidInsertionPoint + } + + let insertionPoint = content.index(after: range.upperBound) + content.insert(contentsOf: " \(newDependencies.joined(separator: "\n "))\n", at: insertionPoint) + + let formattedContent = formatDependencyExtension(content, for: moduleInfo.layer) + + do { + try formattedContent.write(toFile: dependencyFilePath, atomically: true, encoding: .utf8) + print("✅ Dependency added to Dependency+Module.swift") + } catch { + throw ModuleGenerationError.failedToWriteDependencyFile + } + } + + func createDependencyDeclarations(for moduleInfo: ModuleInfo) -> [String] { + let moduleLowercaseName = moduleInfo.moduleName.prefix(1).lowercased() + moduleInfo.moduleName.dropFirst() + var declarations = [ + "static let \(moduleLowercaseName) = TargetDependency.\(moduleInfo.layer.rawValue.lowercased())(name: \"\(moduleInfo.moduleName)\")", + ] + + if moduleInfo.hasInterface { + declarations.append( + "static let \(moduleLowercaseName)Interface = TargetDependency.\(moduleInfo.layer.rawValue.lowercased())(name: \"\(moduleInfo.moduleName)\", targetType: .interface)" + ) + } + + if moduleInfo.hasTestResources { + declarations.append( + "static let \(moduleLowercaseName)TestResources = TargetDependency.\(moduleInfo.layer.rawValue.lowercased())(name: \"\(moduleInfo.moduleName)\", targetType: .testResources)" + ) + } + + return declarations + } + + /// Formats the dependency extension in the Dependency+Module.swift file. + /// + /// This method is responsible for organizing and formatting the dependency declarations + /// within a specific layer's extension in the Dependency+Module.swift file. It performs + /// the following tasks: + /// + /// 1. Identifies the correct extension block for the given layer. + /// 2. Sorts the dependency declarations alphabetically within the extension. + /// 3. Ensures proper indentation and spacing of the declarations. + func formatDependencyExtension(_ content: String, for layer: LayerType) -> String { + let lines = content.components(separatedBy: .newlines) + var formattedLines: [String] = [] + var inTargetExtension = false + var extensionLines: [String] = [] + var commentLine: String? + var emptyLineAdded = false + + for line in lines { + if line.contains("public extension TargetDependency.Module.\(layer.rawValue)") { + inTargetExtension = true + formattedLines.append(line) + } else if inTargetExtension { + if line.contains("}") { + // Add comment and empty line if they exist + if let comment = commentLine { + formattedLines.append(" " + comment) + } + if !emptyLineAdded { + formattedLines.append("") + } + // Sort and format the extension lines + extensionLines.sort() + formattedLines.append(contentsOf: extensionLines.map { " \($0)" }) + formattedLines.append("}") + inTargetExtension = false + } else if line.trimmingCharacters(in: .whitespaces).starts(with: "//") { + commentLine = line.trimmingCharacters(in: .whitespaces) + } else if line.trimmingCharacters(in: .whitespaces).isEmpty { + emptyLineAdded = true + } else if !line.trimmingCharacters(in: .whitespaces).isEmpty { + // Split multiple declarations on the same line + let declarations = line.components(separatedBy: "static let") + .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } + .map { "static let " + $0.trimmingCharacters(in: .whitespaces) } + extensionLines.append(contentsOf: declarations) + } + } else { + formattedLines.append(line) + } + } + + return formattedLines.joined(separator: "\n") + } + + func listCreatedTargets(_ moduleInfo: ModuleInfo) -> String { + var targets = [moduleInfo.moduleName] + + if moduleInfo.hasInterface { + targets.append("\(moduleInfo.moduleName)Interface") + } + if moduleInfo.hasTestResources { + targets.append("\(moduleInfo.moduleName)TestResources") + } + if moduleInfo.hasUnitTests { + targets.append("\(moduleInfo.moduleName)Tests") + } + if moduleInfo.hasUITests { + targets.append("\(moduleInfo.moduleName)UITests") + } + if moduleInfo.hasExample { + targets.append("\(moduleInfo.moduleName)Example") + } + + return targets.joined(separator: ", ") + } + + func runTuistCommand(_ args: [String]) throws { + let process = Process() + process.launchPath = "/usr/bin/env" + process.arguments = ["tuist"] + args + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + do { + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let output = String(data: data, encoding: .utf8) else { + throw ModuleGenerationError.failedToReadTuistOutput + } + print(output) + + if process.terminationStatus != 0 { + throw NSError( + domain: "TuistError", + code: Int(process.terminationStatus), + userInfo: [NSLocalizedDescriptionKey: "Tuist command failed"] + ) + } + } catch { + print("Failed to run Tuist command: \(error)") + throw error + } + } + + func getIntInput(min: Int, max: Int) -> Int { + while true { + print("\u{001B}[1mEnter a number between \(min) and \(max):\u{001B}[0m", terminator: " ") + if let input = readLine(), let number = Int(input), number >= min, number <= max { + return number + } + print("Invalid input. Please try again.") + } + } + + func getBoolInput(_ question: String) -> Bool { + while true { + print("\u{001B}[1m\(question)\u{001B}[0m (y/n):", terminator: " ") + if let input = readLine()?.lowercased() { + switch input { + case "y", "yes": + return true + case "n", "no": + return false + default: + break + } + } + print("Invalid input. Please enter 'y' or 'n'.") + } + } +} + +// swiftlint:enable disable_print + +// MARK: - Main Execution + +let moduleGenerator = ModuleGenerator() +moduleGenerator.generateModule() diff --git a/scripts/git-format-staged b/scripts/git-format-staged new file mode 100755 index 0000000..6644a57 --- /dev/null +++ b/scripts/git-format-staged @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# +# Git command to transform staged files according to a command that accepts file +# content on stdin and produces output on stdout. This command is useful in +# combination with `git add -p` which allows you to stage specific changes in +# a file. This command runs a formatter on the file with staged changes while +# ignoring unstaged changes. +# +# Usage: git-format-staged [OPTION]... [FILE]... +# Example: git-format-staged --formatter 'prettier --stdin-filepath "{}"' '*.js' +# +# Tested with Python versions 3.8 - 3.13. +# +# Original author: Jesse Hallett +# +# Source: https://github.com/hallettj/git-format-staged +# Reason for copying this is that the script can be installed using npm but we don't want that + +from __future__ import print_function +import argparse +from fnmatch import fnmatch +from gettext import gettext as _ +import os +import re +import subprocess +import sys + +# The string $VERSION is replaced during the publish process. +VERSION = '3.1.1' +PROG = sys.argv[0] + +def info(msg): + print(msg, file=sys.stdout) + +def warn(msg): + print('{}: warning: {}'.format(PROG, msg), file=sys.stderr) + +def fatal(msg): + print('{}: error: {}'.format(PROG, msg), file=sys.stderr) + exit(1) + +def format_staged_files(file_patterns, formatter, git_root, update_working_tree=True, write=True, verbose=False): + try: + output = subprocess.check_output([ + 'git', 'diff-index', + '--cached', + '--diff-filter=AM', # select only file additions and modifications + '--no-renames', + 'HEAD' + ]) + for line in output.splitlines(): + entry = parse_diff(line.decode('utf-8')) + entry_path = normalize_path(entry['src_path'], relative_to=git_root) + if entry['dst_mode'] == '120000': + # Do not process symlinks + continue + if not (matches_some_path(file_patterns, entry_path)): + continue + if format_file_in_index(formatter, entry, update_working_tree=update_working_tree, write=write, verbose=verbose): + info('Reformatted {} with {}'.format(entry['src_path'], formatter)) + except Exception as err: + fatal(str(err)) + +# Run formatter on file in the git index. Creates a new git object with the +# result, and replaces the content of the file in the index with that object. +# Returns hash of the new object if formatting produced any changes. +def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=True, verbose=False): + orig_hash = diff_entry['dst_hash'] + new_hash = format_object(formatter, orig_hash, diff_entry['src_path'], verbose=verbose) + + # If the new hash is the same then the formatter did not make any changes. + if not write or new_hash == orig_hash: + return None + + # If the content of the new object is empty then the formatter did not + # produce any output. We want to abort instead of replacing the file with an + # empty one. + if object_is_empty(new_hash): + return None + + replace_file_in_index(diff_entry, new_hash) + + if update_working_tree: + try: + patch_working_file(diff_entry['src_path'], orig_hash, new_hash) + except Exception as err: + # Errors patching working tree files are not fatal + warn(str(err)) + + return new_hash + +file_path_placeholder = re.compile(r'\{\}') + +# Run formatter on a git blob identified by its hash. Writes output to a new git +# blob, and returns the hash of the new blob. +def format_object(formatter, object_hash, file_path, verbose=False): + get_content = subprocess.Popen( + ['git', 'cat-file', '-p', object_hash], + stdout=subprocess.PIPE + ) + command = re.sub(file_path_placeholder, file_path, formatter) + if verbose: + info(command) + format_content = subprocess.Popen( + command, + shell=True, + stdin=get_content.stdout, + stdout=subprocess.PIPE + ) + write_object = subprocess.Popen( + ['git', 'hash-object', '-w', '--stdin'], + stdin=format_content.stdout, + stdout=subprocess.PIPE + ) + + get_content.stdout.close() + format_content.stdout.close() + + if get_content.wait() != 0: + raise ValueError('unable to read file content from object database: ' + object_hash) + + if format_content.wait() != 0: + raise Exception('formatter exited with non-zero status') # TODO: capture stderr from format command + + new_hash, err = write_object.communicate() + + if write_object.returncode != 0: + raise Exception('unable to write formatted content to object database') + + return new_hash.decode('utf-8').rstrip() + +def object_is_empty(object_hash): + get_content = subprocess.Popen( + ['git', 'cat-file', '-p', object_hash], + stdout=subprocess.PIPE + ) + content, err = get_content.communicate() + + if get_content.returncode != 0: + raise Exception('unable to verify content of formatted object') + + return not content + +def replace_file_in_index(diff_entry, new_object_hash): + subprocess.check_call(['git', 'update-index', + '--cacheinfo', '{},{},{}'.format( + diff_entry['dst_mode'], + new_object_hash, + diff_entry['src_path'] + )]) + +def patch_working_file(path, orig_object_hash, new_object_hash): + patch = subprocess.check_output( + ['git', 'diff', '--no-ext-diff', '--color=never', orig_object_hash, new_object_hash] + ) + + # Substitute object hashes in patch header with path to working tree file + patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(new_object_hash.encode(), path.encode()) + + apply_patch = subprocess.Popen( + ['git', 'apply', '-'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + output, err = apply_patch.communicate(input=patch_b) + + if apply_patch.returncode != 0: + raise Exception('could not apply formatting changes to working tree file {}'.format(path)) + +# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path? +diff_pat = re.compile(r'^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t([^\t]+)(?:\t([^\t]+))?$') + +# Parse output from `git diff-index` +def parse_diff(diff): + m = diff_pat.match(diff) + if not m: + raise ValueError('Failed to parse diff-index line: ' + diff) + return { + 'src_mode': unless_zeroed(m.group(1)), + 'dst_mode': unless_zeroed(m.group(2)), + 'src_hash': unless_zeroed(m.group(3)), + 'dst_hash': unless_zeroed(m.group(4)), + 'status': m.group(5), + 'score': int(m.group(6)) if m.group(6) else None, + 'src_path': m.group(7), + 'dst_path': m.group(8) + } + +zeroed_pat = re.compile(r'^0+$') + +# Returns the argument unless the argument is a string of zeroes, in which case +# returns `None` +def unless_zeroed(s): + return s if not zeroed_pat.match(s) else None + +def get_git_root(): + return subprocess.check_output( + ['git', 'rev-parse', '--show-toplevel'] + ).decode('utf-8').rstrip() + +def normalize_path(p, relative_to=None): + return os.path.abspath( + os.path.join(relative_to, p) if relative_to else p + ) + +def matches_some_path(patterns, target): + is_match = False + for signed_pattern in patterns: + (is_pattern_positive, pattern) = from_signed_pattern(signed_pattern) + if fnmatch(target, normalize_path(pattern)): + is_match = is_pattern_positive + return is_match + +# Checks for a '!' as the first character of a pattern, returns the rest of the +# pattern in a tuple. The tuple takes the form (is_pattern_positive, pattern). +# For example: +# from_signed_pattern('!pat') == (False, 'pat') +# from_signed_pattern('pat') == (True, 'pat') +def from_signed_pattern(pattern): + if pattern[0] == '!': + return (False, pattern[1:]) + else: + return (True, pattern) + +class CustomArgumentParser(argparse.ArgumentParser): + def parse_args(self, args=None, namespace=None): + args, argv = self.parse_known_args(args, namespace) + if argv: + msg = argparse._( + 'unrecognized arguments: %s. Do you need to quote your formatter command?' + ) + self.error(msg % ' '.join(argv)) + return args + +if __name__ == '__main__': + parser = CustomArgumentParser( + description='Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.', + epilog='Example: %(prog)s --formatter "prettier --stdin-filepath \'{}\'" "src/*.js" "test/*.js"' + ) + parser.add_argument( + '--formatter', '-f', + required=True, + help='Shell command to format files, will run once per file. Occurrences of the placeholder `{}` will be replaced with a path to the file being formatted. (Example: "prettier --stdin-filepath \'{}\'")' + ) + parser.add_argument( + '--no-update-working-tree', + action='store_true', + help='By default formatting changes made to staged file content will also be applied to working tree files via a patch. This option disables that behavior, leaving working tree files untouched.' + ) + parser.add_argument( + '--no-write', + action='store_true', + help='Prevents %(prog)s from modifying staged or working tree files. You can use this option to check staged changes with a linter instead of formatting. With this option stdout from the formatter command is ignored. Example: %(prog)s --no-write -f "eslint --stdin --stdin-filename \'{}\' >&2" "*.js"' + ) + parser.add_argument( + '--version', + action='version', + version='%(prog)s version {}'.format(VERSION), + help='Display version of %(prog)s' + ) + parser.add_argument( + '--verbose', + help='Show the formatting commands that are running', + action='store_true' + ) + parser.add_argument( + 'files', + nargs='+', + help='Patterns that specify files to format. The formatter will only transform staged files that are given here. Patterns may be literal file paths, or globs which will be tested against staged file paths using Python\'s fnmatch function. For example "src/*.js" will match all files with a .js extension in src/ and its subdirectories. Patterns may be negated to exclude files using a "!" character. Patterns are evaluated left-to-right. (Example: "main.js" "src/*.js" "test/*.js" "!test/todo/*")' + ) + args = parser.parse_args() + files = vars(args)['files'] + format_staged_files( + file_patterns=files, + formatter=vars(args)['formatter'], + git_root=get_git_root(), + update_working_tree=not vars(args)['no_update_working_tree'], + write=not vars(args)['no_write'], + verbose=vars(args)['verbose'] + ) \ No newline at end of file diff --git a/scripts/rename.sh b/scripts/rename.sh new file mode 100755 index 0000000..79014fb --- /dev/null +++ b/scripts/rename.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +original=STRVTemplate +# Used for Keychain +keysOriginal=strv_template +updated=`echo $1 | sed 's/ /_/g'` +updated_lowercase=`echo $updated | tr '[:upper:]' '[:lower:]'` + +echo "Starting renaming of STRVTemplate -> $1" + +# Replace strings in all files (except specific directories) +echo "Replacing STRVTemplate in all files..." +find . -type f -not -path "./.git/*" -not -path "./.githooks*" -not -path "./Pods*" -not -path "./scripts*" -not -path "./vendor*" -not -path "./.bundle*" -not -path "./Tuist/.build/*" -print0 | while IFS= read -r -d '' file; do + # First, copy the file with its permissions + cp "$file" "$file.tmp" + sed "s/$original/$updated/g; s/$keysOriginal/$updated_lowercase/g" "$file" 2> /dev/null 1> "$file.tmp" + size=`stat -f%z "$file.tmp"` + if [ "$size" = "0" ]; then + rm "$file.tmp" + else + rm "$file" + mv "$file.tmp" "$file" + fi +done + +# Rename directories +echo "Renaming directories..." +find . -type d -name "*$original*" -print0 | while IFS= read -r -d '' file; do + new=`echo "$file" | sed "s/$original/$updated/g"` + mv "$file" "$new" +done + +# Rename files +echo "Renaming files..." +find . -type f -name "*$original*" -print0 | while IFS= read -r -d '' file; do + new=`echo "$file" | sed "s/$original/$updated/g"` + mv "$file" "$new" +done + +echo "Renaming process completed." \ No newline at end of file diff --git a/scripts/setup_bundler.sh b/scripts/setup_bundler.sh new file mode 100755 index 0000000..f783f37 --- /dev/null +++ b/scripts/setup_bundler.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +desired_bundler_version=$(cat .bundler-version) +installed_bundler_version=$(gem list bundler | grep -w $desired_bundler_version) + +if [ -z "$installed_bundler_version" ]; then + echo "Bundler version $desired_bundler_version is not installed. Installing now..." + gem install bundler -v $desired_bundler_version +else + echo "Found correct Bundler version: $desired_bundler_version" +fi \ No newline at end of file diff --git a/scripts/setup_ci.sh b/scripts/setup_ci.sh new file mode 100755 index 0000000..57535fc --- /dev/null +++ b/scripts/setup_ci.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +if [ "$1" == "enterprise" ] || [ "$1" == "testflight" ] || [ "$1" == "integrations" ]; then + [[ $1 = "integrations" ]] && yml="$1.yml" || yml="$1-deployment.yml" + + mkdir -p .github/workflows + + # Copy environment file if it exists + if [ -f "fastlane/sample.env.$1" ]; then + cp "fastlane/sample.env.$1" "fastlane/.env.$1" + fi + + # Copy workflow file if it exists + if [ -f ".github/sample-workflows/$yml" ]; then + cp ".github/sample-workflows/$yml" ".github/workflows/$yml" + + if [ "$1" != "integrations" ]; then + echo "🟡 Configure .github/workflows/$yml and provide secrets via Github secrets if needed." + echo "If you want to run fastlane locally, make sure that fastlane/.env.$1 is .gitignored and contains all necessary secrets" + fi + else + if [ "$1" == "integrations" ]; then + echo "💡 Integrations workflows are already set up in .github/workflows/" + else + echo "⚠️ Warning: .github/sample-workflows/$yml not found" + fi + fi +else + echo "Provide a CI type: 'enterprise', 'testflight' or 'integrations'." +fi diff --git a/scripts/setup_tuist_githook.sh b/scripts/setup_tuist_githook.sh new file mode 100755 index 0000000..b7c8055 --- /dev/null +++ b/scripts/setup_tuist_githook.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Script for generating project files from the Tuist definition files +script=" +# Tuist +if [ -f \"App/Project.swift\" ]; then + tuist generate --no-open +fi" + +# Destination file where the script should be set +hookFile=".githooks/post-checkout" + +if [ ! -f "$hookFile" ]; then + # post-checkout git hook file doesn't exist - create file, add header, and script + echo "#!/bin/bash" > "$hookFile" + echo "$script" >> "$hookFile" +elif ! grep -q "Tuist" "$hookFile"; then + # File exists but the Tuist section is missing - add just the script + echo "$script" >> "$hookFile" +fi + +# Hooks needs to be executable +chmod +x .githooks/post-checkout \ No newline at end of file diff --git a/scripts/swiftformat.sh b/scripts/swiftformat.sh new file mode 100755 index 0000000..a467bb5 --- /dev/null +++ b/scripts/swiftformat.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Description: +# This script facilitates the running of SwiftFormat. +# It checks for the correct version of SwiftFormat, and runs SwiftFormat on git staged files using 'git-format-staged' binary. +# The desired version of SwiftFormat is specified in a '.swiftformat-version' file located in the project root directory. + +# Usage: +# This script is executed during pre-commit git hook in order to prevent the loss of Xcode's undo functionality + +# Exit Codes: +# - 0: SwiftFormat ran successfully or script exited early in a CI environment. +# - 1: An error occurred, such as SwiftFormat not being installed or version mismatch. + +if [ $CI ]; then + echo "No need to run SwiftFormat on CI" + exit 0 +fi + +cd "$(dirname "$0")/.." + +eval "$($HOME/.local/bin/mise activate -C $SRCROOT bash --shims)" + +swiftformat_bin=$(mise which swiftformat) + +if ! mise which swiftformat >/dev/null; then + echo "error: SwiftFormat is not installed. Install by running setup.swift install or setup.swift install-dependencies" + exit 1 +fi + +desired_version=$(mise current swiftformat) +current_version=$(mise exec -- swiftformat --version) + +if [[ $current_version != $desired_version ]]; then + echo "error: SwiftFormat version mismatch. Expected $desired_version but found $current_version." + echo "error: Install the correct version by running setup.swift install" + exit 1 +fi + +# Run SwiftFormat on git-staged files +cd "$(git rev-parse --show-toplevel)" + +./scripts/git-format-staged --formatter "$swiftformat_bin stdin --stdinpath '{}'" "*.swift" \ No newline at end of file diff --git a/scripts/swiftlint.sh b/scripts/swiftlint.sh new file mode 100755 index 0000000..3a26f40 --- /dev/null +++ b/scripts/swiftlint.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Description: +# This script facilitates the running of SwiftLint. +# It checks for the correct version of SwiftLint, and runs SwiftLint. +# The desired version of SwiftLint is fetched directly via mise. + +# Usage: +# This script is run as a pre-build phase script. +# In a CI environment, SwiftLint will be run by Danger instead. + +# Exit Codes: +# - 0: SwiftLint ran successfully or script exited early in a CI environment. +# - 1: An error occurred, such as SwiftLint not being installed or version mismatch. + +if [ $CI ]; then + echo "No need to run SwiftLint, Danger will run it!" + exit 0 +fi + +cd "$(dirname "$0")/.." + +# Activate Mise so that Xcode can find it (https://mise.jdx.dev/ide-integration.html#xcode) +eval "$($HOME/.local/bin/mise activate -C $SRCROOT bash --shims)" + +if ! mise which swiftlint >/dev/null; then + echo "error: SwiftLint is not installed. Install by running setup.swift install or setup.swift install-dependencies" + exit 1 +fi + +desired_version=$(mise current swiftlint) +current_version=$(mise exec -- swiftlint --version) + +if [[ $current_version != $desired_version ]]; then + echo "error: SwiftLint version mismatch. Expected $desired_version but found $current_version." + echo "error: Install the correct version by running setup.swift install" + exit 1 +fi + +# Run SwiftLint +mise exec -- swiftlint \ No newline at end of file diff --git a/scripts/update_dependencies.rb b/scripts/update_dependencies.rb new file mode 100644 index 0000000..84eaa48 --- /dev/null +++ b/scripts/update_dependencies.rb @@ -0,0 +1,128 @@ +# update_dependencies.rb +# +# Description: +# This script updates the versions of our development tools and dependencies. +# It currently supports SwiftLint, SwiftFormat, Tuist, and Cocoapods if specified in a Gemfile. +# +# Usage: +# This script is run via the `swift setup.swift update-tools` +# Ensure Gemfile is present if updating Cocoapods version. +# +# Note: +# Cocoapods are updated via the `setup.swift`` but Swiftlint + SwiftFormat and deliberately updated +# via their specific scripts to limit the number of requests that need to be done to Github. +# There is a limit rate to unauthorized requests per IP address of 60/hour. + +require 'json' +require 'net/http' +require 'uri' + +# Definitions +def get_current_version(tool_name) + # Fetch current version using mise current + `mise current #{tool_name}`.strip +end + +def get_latest_version(tool_name) + # Fetch latest version using mise latest + `mise latest #{tool_name}`.strip +end + +def success_message(tool, current_version, latest_version) + "🟢 Updated #{tool} from #{current_version} to #{latest_version}" +end + +def no_update_message(tool, current_version) + "🟡 No update needed for #{tool}. Current version matches the latest version: #{current_version}" +end + +def failure_message(tool, error_message) + "🔴 Failed to update #{tool}. #{error_message}" +end + +def gemfile_contains_cocoapods?(gemfile_content) + !!gemfile_content.match(/gem 'cocoapods'/) +end + +def update_gemfile_with_cocoapods_version(cocoapods_version, gemfile_content) + updated_content = gemfile_content.gsub(/gem 'cocoapods', '.*'/, "gem 'cocoapods', '#{cocoapods_version}'") + File.write('Gemfile', updated_content) +end + +def update_cocoapods_version(gemfile_content) + if gemfile_contains_cocoapods?(gemfile_content) + current_cocoapods_version_match = gemfile_content.match(/gem 'cocoapods', '(.*?)'/) + + if current_cocoapods_version_match + current_cocoapods_version = current_cocoapods_version_match[1] + latest_cocoapods_version = `gem info cocoapods --remote | grep '^cocoapods' | awk '{print $2}' | tr -d '()'`.strip + + if latest_cocoapods_version.empty? + puts failure_message('Cocoapods', 'Unable to fetch latest version.') + elsif current_cocoapods_version != latest_cocoapods_version + update_gemfile_with_cocoapods_version(latest_cocoapods_version, gemfile_content) + puts success_message('Cocoapods', current_cocoapods_version, latest_cocoapods_version) + + # Update Cocoapods by installing the bundle + system('swift setup.swift install-bundle') + system('bundle exec pod install') + else + puts no_update_message('Cocoapods', current_cocoapods_version) + end + else + puts 'Error parsing current Cocoapods version in Gemfile.' + end + else + puts "Gemfile doesn't contain Cocoapods" + end +end + +def update_mise_self + puts "Running Mise self-update..." + system('mise self-update') +end + +# Update Mise +update_mise_self + +# Tools +tools = ['swiftlint', 'swiftformat', 'tuist'] + +# Update each of the tools specified above +tools.each do |mise_key| + begin + # Fetch current and latest versions using mise commands + current_version = get_current_version(mise_key) + latest_version = get_latest_version(mise_key) + + if latest_version.empty? + puts failure_message(mise_key.capitalize, 'Unable to fetch the latest version using mise latest.') + next + end + + if current_version != latest_version + # Use mise use to set the tool to the latest version + system("mise use #{mise_key}@#{latest_version}") + + # Success message + puts success_message(mise_key.capitalize, current_version, latest_version) + else + # No update needed message + puts no_update_message(mise_key.capitalize, current_version) + end + rescue StandardError => e + puts failure_message(mise_key.capitalize, e.message) + end +end + +# Update Cocoapods if found in Gemfile +if File.exist?('Gemfile') + begin + gemfile_content = File.read('Gemfile') + update_cocoapods_version(gemfile_content) + rescue StandardError => e + puts failure_message('Cocoapods', e.message) + end +else + puts 'Gemfile does not exist.' +end \ No newline at end of file diff --git a/scripts/verify_ruby.sh b/scripts/verify_ruby.sh new file mode 100755 index 0000000..24dae9f --- /dev/null +++ b/scripts/verify_ruby.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Description: +# This script ensures, the recommended Ruby version specified in a .ruby-version file, is installed and active for this repository. +# If the required version is not found, the script attempts to install it using the detected Ruby version manager (rbenv, rvm, or chruby). + +recommended_ruby_version=$(cat .ruby-version) +# Attempt to get the current Ruby version, capturing any error output +current_ruby_version=$(ruby -e 'puts RUBY_VERSION' 2>&1) + +# Function to check which Ruby version manager is in use +detect_ruby_manager() { + if command -v rbenv >/dev/null 2>&1; then + echo "rbenv" + elif command -v rvm >/dev/null 2>&1; then + echo "rvm" + elif command -v chruby >/dev/null 2>&1; then + echo "chruby" + else + echo "none" + fi +} + +install_ruby_version() { + local version=$1 + local manager=$2 + + case $manager in + rbenv) + echo "Attempting to install Ruby $version using rbenv..." + brew update && brew upgrade rbenv ruby-build # make sure that we have latest ruby-build + rbenv install "$version" && rbenv local "$version" + ;; + rvm) + echo "Attempting to install Ruby $version using rvm..." + rvm install "$version" && rvm use "$version" + ;; + chruby) + echo "Attempting to install Ruby $version using ruby-install..." + ruby-install ruby-"$version" && chruby ruby-"$version" + ;; + *) + echo "Unable to install Ruby $version. Unsupported Ruby version manager." + return 1 + ;; + esac +} + +# Function to output a warning message and instructions based on the Ruby manager +output_warning() { + local recommended_ruby_version=$1 + local ruby_manager=$2 + + echo "⚠️ Recommended Ruby version $recommended_ruby_version is not installed." + + # ANSI escape code to start bold and underline text + local bold_underline="\033[1;4m" + + # ANSI escape code to reset text formatting + local reset="\033[0m" + + case $ruby_manager in + rbenv) + echo -e "Please install it using rbenv with the following command:" + echo -e "${bold_underline}rbenv install $recommended_ruby_version${reset}" + ;; + rvm) + echo -e "Please install it using rvm with the following command:" + echo -e "${bold_underline}rvm install $recommended_ruby_version${reset}" + ;; + chruby) + echo -e "Please install it using ruby-install with the following command:" + echo -e "${bold_underline}ruby-install ruby-$recommended_ruby_version${reset}" + ;; + *) + echo "Please install it using your Ruby version manager." + ;; + esac +} + +# If the current Ruby version matches the recommended version, we're done +if [[ "$current_ruby_version" == "$recommended_ruby_version" ]]; then + echo "🟢 Found recommended Ruby version: $recommended_ruby_version" + exit 0 +fi + +ruby_manager=$(detect_ruby_manager) + +# Attempt to install the recommended Ruby version +install_ruby_version "$recommended_ruby_version" "$ruby_manager" +install_result=$? + +# Check the Ruby version again after attempting to install +current_ruby_version=$(ruby -e 'puts RUBY_VERSION' 2>&1) + +if [[ "$current_ruby_version" == "$recommended_ruby_version" ]]; then + echo "🟢 Successfully installed and switched to recommended Ruby version: $recommended_ruby_version" + exit 0 +elif [[ $install_result -eq 0 ]]; then + echo "⚠️ Installed Ruby $recommended_ruby_version, but failed to switch to it. Please check your Ruby version manager." + exit 1 +else + output_warning "$recommended_ruby_version" "$ruby_manager" + exit 1 +fi \ No newline at end of file From 7b29edf9e59e01b148ae0781eaead656045efdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:56:40 +0100 Subject: [PATCH 36/52] fix: githooks --- .githooks/pre-commit | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..73a926f --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,4 @@ +#!/bin/bash + +cd "$(git rev-parse --show-toplevel)" +./scripts/swiftformat.sh \ No newline at end of file From 4f0fcf03b08ba9f66d010a3c7389b437f4403339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 11:58:20 +0100 Subject: [PATCH 37/52] fix: gemfile and dangerfile --- Dangerfile | 44 +++++++++++++++++++++++++++++++++++++++----- Gemfile | 17 +++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 Gemfile diff --git a/Dangerfile b/Dangerfile index 8d1d444..519857e 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,14 +1,14 @@ -# By Jan Schwarz 03/24/2021 -# STRV s.r.o. 2021 +# By Jakub Kaspar 10/30/2018 +# STRV s.r.o. 2018 # STRV # Sometimes its a README fix, or something like that - which isn't relevant for including in a CHANGELOG for example declared_trivial = github.pr_title.include?("#trivial") || github.pr_title.include?("[WIP]") || github.pr_body.include?("#trivial") # Uncomment following lines if you want to enforce CHANGELOG updates -if git.lines_of_code > 50 && !git.modified_files.include?("CHANGELOG.md") && !git.added_files.include?("CHANGELOG.md") && !declared_trivial - fail("No CHANGELOG changes made") -end +# if git.lines_of_code > 50 && !git.modified_files.include?("CHANGELOG.md") && !git.added_files.include?("CHANGELOG.md") && !declared_trivial +# fail("No CHANGELOG changes made") +# end # Make it more obvious that a PR is a work in progress and shouldn't be merged yet warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" @@ -22,3 +22,37 @@ end if git.lines_of_code > 1000 warn("Big PR - you should create smaller!") end + +# By default we have SwiftLint in our vendor folder. + +# Projects with SwiftLint via SPM plugin without using mise +# +# If you are using Swiftlint with SPM (for some reason) you must omit the `swiftlint.binary_path` (eg. delete the lines 35-42) +# Also be sure to specify the correct version in `.swiftlint-version` file +# and you must have the `Set SwiftLint version` step in the integrations GitHub Action +# by doing that "Danger Swiftlint" will install the desired version itself + +# Determine the SwiftLint binary path dynamically using mise +swiftlint_binary_path = `mise which swiftlint`.strip +# Set SwiftLint binary path if found +if swiftlint_binary_path.empty? + warn("SwiftLint binary not found, using default path") +else + swiftlint.binary_path = swiftlint_binary_path +end + +swiftlint.config_file = '.github/config/.swiftlint.yml' +swiftlint.lint_files inline_mode:true, fail_on_error:true + +duplicate_localizable_strings.check_localizable_duplicates + +# Determine the SwiftFormat binary path dynamically using mise +swiftformat_binary_path = `mise which swiftformat`.strip +# Set SwiftFormat binary path if found +if swiftformat_binary_path.empty? + warn("SwiftFormat binary not found, using default path") +else + swiftformat.binary_path = swiftformat_binary_path +end +# Runs SwiftFormat on changed files +swiftformat.check_format(fail_on_error: true) \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..90066f4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,17 @@ +# STRV s.r.o. 2019 +# STRV + +source 'https://rubygems.org' + +# Ruby 3.4+ compatibility +gem 'abbrev' +gem 'nkf' + +gem 'danger', '9.5.3' +gem 'danger-duplicate_localizable_strings', git: 'https://github.com/strvcom/ios_danger-duplicate_localizable_strings', ref: 'master' +gem 'danger-swiftlint', '0.37.2' +gem 'danger-swiftformat', git: 'https://github.com/strvcom/ios-danger-ruby-swiftformat.git', ref: 'master' +gem 'fastlane', '2.228.0' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) From 94e82b1abc865575f806f4600cdd56c64b78b443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 12:20:35 +0100 Subject: [PATCH 38/52] fix: mise.toml & formatting --- .mise.toml | 1 + Sources/Container/Async/AsyncContainer.swift | 1 - Sources/Container/Sync/Container.swift | 1 - Sources/Models/RegistrationIdentifier.swift | 4 ++-- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.mise.toml b/.mise.toml index ccb587f..e56e919 100644 --- a/.mise.toml +++ b/.mise.toml @@ -2,6 +2,7 @@ ruby = "3.4.6" swiftlint = "0.57.0" swiftformat = "0.54.5" +tuist = "4.65.6" [settings] legacy_version_file = false diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index c57fc63..abd6b90 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -124,7 +124,6 @@ private extension AsyncContainer { 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 f59fc12..59d62d4 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -120,7 +120,6 @@ private extension Container { 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/RegistrationIdentifier.swift b/Sources/Models/RegistrationIdentifier.swift index 4a7dbc7..8ad361b 100644 --- a/Sources/Models/RegistrationIdentifier.swift +++ b/Sources/Models/RegistrationIdentifier.swift @@ -22,7 +22,7 @@ struct RegistrationIdentifier: Sendable { /// - 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) { + init(type: Dependency.Type, argumentTypes _: repeat (each Argument).Type) { typeIdentifier = ObjectIdentifier(type) var identifiers: [ObjectIdentifier] = [] @@ -52,7 +52,7 @@ extension RegistrationIdentifier: CustomStringConvertible { let argumentsDescription: String = if argumentIdentifiers.isEmpty { "nil" } else { - argumentIdentifiers.map { $0.debugDescription }.joined(separator: ", ") + argumentIdentifiers.map(\.debugDescription).joined(separator: ", ") } return """ Type: \(typeIdentifier.debugDescription) From 7318f3578ff16b4b734108c8a25238a1c6e00297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 13:01:58 +0100 Subject: [PATCH 39/52] fix: setup install --- Gemfile.lock | 324 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.swift | 1 - 2 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..e2770e1 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,324 @@ +GIT + remote: https://github.com/strvcom/ios-danger-ruby-swiftformat.git + revision: 3183ee3b72cf7884d046eb1827e3527dd9b0fbb0 + ref: master + specs: + danger-swiftformat (0.9.0) + danger-plugin-api (~> 1.0) + +GIT + remote: https://github.com/strvcom/ios_danger-duplicate_localizable_strings + revision: 4e8153f4a3e03f441230657469ca1609c48fdb75 + ref: master + specs: + danger-duplicate_localizable_strings (0.4.1) + danger (~> 9.0) + +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + activesupport (8.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1220.0) + aws-sdk-core (3.242.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.122.0) + aws-sdk-core (~> 3, >= 3.241.4) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.213.0) + aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + bigdecimal (4.0.1) + 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) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + cork (0.3.0) + colored2 (~> 3.1) + danger (9.5.3) + base64 (~> 0.2) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (>= 3.1, < 5) + cork (~> 0.1) + faraday (>= 0.9.0, < 3.0) + faraday-http-cache (~> 2.0) + git (>= 1.13, < 3.0) + kramdown (>= 2.5.1, < 3.0) + kramdown-parser-gfm (~> 1.0) + octokit (>= 4.0) + pstore (~> 0.1) + terminal-table (>= 1, < 5) + danger-plugin-api (1.0.0) + danger (> 2.0) + danger-swiftlint (0.37.2) + danger + rake (> 10) + thor (~> 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) + drb (2.2.3) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.5) + 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.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-http-cache (2.6.1) + faraday (>= 0.8) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.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.228.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 (~> 1.2) + 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) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + 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, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + 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.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + git (2.3.3) + activesupport (>= 5.0) + addressable (~> 2.8) + process_executer (~> 1.1) + rchardet (~> 1.8) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + 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.9.0) + mutex_m + i18n (1.14.8) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.18.1) + jwt (2.10.2) + base64 + kramdown (2.5.2) + rexml (>= 3.4.4) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + multi_json (1.19.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + nap (1.1.0) + naturally (2.3.0) + nkf (0.2.0) + octokit (10.0.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) + open4 (1.3.4) + optparse (0.8.1) + os (1.1.4) + plist (3.7.2) + prism (1.9.0) + process_executer (1.3.0) + pstore (0.2.1) + public_suffix (7.0.2) + rake (13.3.1) + rchardet (1.10.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.2.1) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + sawyer (0.9.3) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + securerandom (0.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + thor (1.5.0) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (2.6.0) + uri (1.1.1) + 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.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + abbrev + danger (= 9.5.3) + danger-duplicate_localizable_strings! + danger-swiftformat! + danger-swiftlint (= 0.37.2) + fastlane (= 2.228.0) + nkf + +BUNDLED WITH + 2.5.17 diff --git a/setup.swift b/setup.swift index 987ed44..da4225e 100755 --- a/setup.swift +++ b/setup.swift @@ -238,7 +238,6 @@ private extension CommandRunner { try await installMiseDependencies(shouldVerifyMise: false) try await installBundle(shouldVerifyMise: false) try await installTuistDependencies(isUsingTuist: tuistDefinitionFileExists) - try installVersionIconIfNeeded() } func installBundle(shouldVerifyMise: Bool) async throws { From d7d6c078f96269b54067520ab05286de06299028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 13:21:39 +0100 Subject: [PATCH 40/52] fix: dangerfile --- Dangerfile | 44 +++++--------------------------------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/Dangerfile b/Dangerfile index 519857e..8d1d444 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,14 +1,14 @@ -# By Jakub Kaspar 10/30/2018 -# STRV s.r.o. 2018 +# By Jan Schwarz 03/24/2021 +# STRV s.r.o. 2021 # STRV # Sometimes its a README fix, or something like that - which isn't relevant for including in a CHANGELOG for example declared_trivial = github.pr_title.include?("#trivial") || github.pr_title.include?("[WIP]") || github.pr_body.include?("#trivial") # Uncomment following lines if you want to enforce CHANGELOG updates -# if git.lines_of_code > 50 && !git.modified_files.include?("CHANGELOG.md") && !git.added_files.include?("CHANGELOG.md") && !declared_trivial -# fail("No CHANGELOG changes made") -# end +if git.lines_of_code > 50 && !git.modified_files.include?("CHANGELOG.md") && !git.added_files.include?("CHANGELOG.md") && !declared_trivial + fail("No CHANGELOG changes made") +end # Make it more obvious that a PR is a work in progress and shouldn't be merged yet warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" @@ -22,37 +22,3 @@ end if git.lines_of_code > 1000 warn("Big PR - you should create smaller!") end - -# By default we have SwiftLint in our vendor folder. - -# Projects with SwiftLint via SPM plugin without using mise -# -# If you are using Swiftlint with SPM (for some reason) you must omit the `swiftlint.binary_path` (eg. delete the lines 35-42) -# Also be sure to specify the correct version in `.swiftlint-version` file -# and you must have the `Set SwiftLint version` step in the integrations GitHub Action -# by doing that "Danger Swiftlint" will install the desired version itself - -# Determine the SwiftLint binary path dynamically using mise -swiftlint_binary_path = `mise which swiftlint`.strip -# Set SwiftLint binary path if found -if swiftlint_binary_path.empty? - warn("SwiftLint binary not found, using default path") -else - swiftlint.binary_path = swiftlint_binary_path -end - -swiftlint.config_file = '.github/config/.swiftlint.yml' -swiftlint.lint_files inline_mode:true, fail_on_error:true - -duplicate_localizable_strings.check_localizable_duplicates - -# Determine the SwiftFormat binary path dynamically using mise -swiftformat_binary_path = `mise which swiftformat`.strip -# Set SwiftFormat binary path if found -if swiftformat_binary_path.empty? - warn("SwiftFormat binary not found, using default path") -else - swiftformat.binary_path = swiftformat_binary_path -end -# Runs SwiftFormat on changed files -swiftformat.check_format(fail_on_error: true) \ No newline at end of file From 0c7348b063d6e371e47f03edaa684f9a89e6a374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Mon, 2 Mar 2026 13:26:39 +0100 Subject: [PATCH 41/52] fix: setup of ci --- .github/workflows/pr-integrations.yml | 4 +--- Gemfile | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml index f124be7..2b77175 100644 --- a/.github/workflows/pr-integrations.yml +++ b/.github/workflows/pr-integrations.yml @@ -101,6 +101,4 @@ jobs: # Run Unit Tests - name: Run Unit Tests - run: bundle exec fastlane tests without_dependencies:true - env: - TEST_PLAN: UnitTestPlan + run: swift test --enable-code-coverage diff --git a/Gemfile b/Gemfile index 90066f4..3b313e2 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,4 @@ gem 'danger', '9.5.3' gem 'danger-duplicate_localizable_strings', git: 'https://github.com/strvcom/ios_danger-duplicate_localizable_strings', ref: 'master' gem 'danger-swiftlint', '0.37.2' gem 'danger-swiftformat', git: 'https://github.com/strvcom/ios-danger-ruby-swiftformat.git', ref: 'master' -gem 'fastlane', '2.228.0' -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) From c2105dc94650df28728d432b2858327b0cf38f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 13:01:55 +0100 Subject: [PATCH 42/52] remove: scripts --- .githooks/pre-commit | 4 - scripts/ModuleGenerator.swift | 404 ----------------- scripts/git-format-staged | 282 ------------ scripts/rename.sh | 40 -- scripts/setup_bundler.sh | 11 - scripts/setup_ci.sh | 30 -- scripts/setup_tuist_githook.sh | 23 - scripts/swiftformat.sh | 43 -- scripts/swiftlint.sh | 41 -- scripts/update_dependencies.rb | 128 ------ scripts/verify_ruby.sh | 105 ----- setup.swift | 761 --------------------------------- 12 files changed, 1872 deletions(-) delete mode 100755 .githooks/pre-commit delete mode 100755 scripts/ModuleGenerator.swift delete mode 100755 scripts/git-format-staged delete mode 100755 scripts/rename.sh delete mode 100755 scripts/setup_bundler.sh delete mode 100755 scripts/setup_ci.sh delete mode 100755 scripts/setup_tuist_githook.sh delete mode 100755 scripts/swiftformat.sh delete mode 100755 scripts/swiftlint.sh delete mode 100644 scripts/update_dependencies.rb delete mode 100755 scripts/verify_ruby.sh delete mode 100755 setup.swift diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index 73a926f..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -cd "$(git rev-parse --show-toplevel)" -./scripts/swiftformat.sh \ No newline at end of file diff --git a/scripts/ModuleGenerator.swift b/scripts/ModuleGenerator.swift deleted file mode 100755 index 95d2153..0000000 --- a/scripts/ModuleGenerator.swift +++ /dev/null @@ -1,404 +0,0 @@ -#!/usr/bin/swift -import Foundation - -// swiftlint:disable disable_print - -// This enables the script to exit when the user presses Ctrl+C -signal(SIGINT) { _ in exit(0) } - -// MARK: - Enums - -enum LayerType: String, CaseIterable { - case feature = "Feature" - case service = "Service" - case library = "Library" -} - -enum ModuleGenerationError: Error { - case failedToReadDependencyFile - case failedToWriteDependencyFile - case invalidInsertionPoint - case failedToReadTuistOutput -} - -// MARK: - Structs - -struct ModuleInfo { - let layer: LayerType - let moduleName: String - let hasInterface: Bool - let hasTestResources: Bool - let hasUnitTests: Bool - let hasUITests: Bool - let hasExample: Bool - let hasLocalizedStrings: Bool - let hasAssets: Bool -} - -// MARK: - Module Generator - -/// This class utilizes Tuist's `scaffold` command to generate the basic structure and files -/// for new modules in a modularized project. It automates the process of creating modules, -/// their interfaces, tests, and example targets based on provided specifications. -final class ModuleGenerator { - private let fileManager = FileManager.default - private let currentPath = "./" - - func generateModule() { - do { - let moduleInfo = getModuleInfo() - try scaffoldModule(moduleInfo) - try registerDependency(moduleInfo) - - let createdTargets = listCreatedTargets(moduleInfo) - print("✅ Module '\(moduleInfo.moduleName)' created successfully in the \(moduleInfo.layer.rawValue) layer!") - print(" Created targets: \(createdTargets)") - } catch { - print("❌ Error creating module: \(error)") - } - } -} - -private extension ModuleGenerator { - func getModuleInfo() -> ModuleInfo { - print("\u{001B}[1mWhat's the layer of this module?\u{001B}[0m") - for (index, layer) in LayerType.allCases.enumerated() { - print("\(index + 1). \(layer.rawValue)") - } - - let layer = LayerType.allCases[getIntInput(min: 1, max: LayerType.allCases.count) - 1] - - print("\u{001B}[1mEnter module name:\u{001B}[0m", terminator: " ") - let moduleName = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - - let hasResources = getBoolInput("Do you want to include resources (strings or assets) in this module?") - var hasLocalizedStrings = false - var hasAssets = false - - if hasResources { - hasLocalizedStrings = getBoolInput("Do you want to include localized strings?") - hasAssets = getBoolInput("Do you want to include an Asset catalog (for colors and images)?") - } - - let hasInterface = getBoolInput("Do you want to include an Interface target?") - let hasUnitTests = getBoolInput("Do you want to include unit tests target?") - let hasUITests = getBoolInput("Do you want to include UI tests target?") - let hasTestResources = if hasUnitTests || hasUITests { - getBoolInput("Do you need a TestResources target for test utilities and mocks?") - } else { - false - } - - let hasExample = hasUITests || getBoolInput("Do you want to create an Example target (for a demo app)?") - - print("\u{001B}[1m⏳ Generating...:\u{001B}[0m\n\n", terminator: " ") - - return ModuleInfo( - layer: layer, - moduleName: moduleName, - hasInterface: hasInterface, - hasTestResources: hasTestResources, - hasUnitTests: hasUnitTests, - hasUITests: hasUITests, - hasExample: hasExample, - hasLocalizedStrings: hasLocalizedStrings, - hasAssets: hasAssets - ) - } - - func scaffoldModule(_ moduleInfo: ModuleInfo) throws { - try scaffoldMainModule(moduleInfo) - - if moduleInfo.hasInterface { try scaffoldInterface(moduleInfo) } - if moduleInfo.hasTestResources { try scaffoldTestResources(moduleInfo) } - if moduleInfo.hasUnitTests { try scaffoldTests(moduleInfo) } - if moduleInfo.hasUITests { try scaffoldUITests(moduleInfo) } - if moduleInfo.hasExample { try scaffoldExample(moduleInfo) } - if moduleInfo.hasLocalizedStrings { try scaffoldLocalizedStrings(moduleInfo) } - if moduleInfo.hasAssets { try scaffoldAssets(moduleInfo) } - } - - func scaffoldMainModule(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "Module", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - "--interface", moduleInfo.hasInterface.description, - "--testresources", moduleInfo.hasTestResources.description, - "--tests", moduleInfo.hasUnitTests.description, - "--uitests", moduleInfo.hasUITests.description, - "--example", moduleInfo.hasExample.description, - "--localizedstrings", moduleInfo.hasLocalizedStrings.description, - "--assets", moduleInfo.hasAssets.description, - ] - - try runTuistCommand(args) - } - - func scaffoldInterface(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "Interface", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - ] - - try runTuistCommand(args) - } - - func scaffoldTestResources(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "TestResources", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - ] - - try runTuistCommand(args) - } - - func scaffoldTests(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "Tests", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - ] - - try runTuistCommand(args) - } - - func scaffoldUITests(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "UITests", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - ] - - try runTuistCommand(args) - } - - func scaffoldExample(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "Example", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - ] - - try runTuistCommand(args) - } - - func scaffoldLocalizedStrings(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "Localizable", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - ] - - try runTuistCommand(args) - } - - func scaffoldAssets(_ moduleInfo: ModuleInfo) throws { - let args = [ - "scaffold", - "Assets", - "--layer", moduleInfo.layer.rawValue, - "--name", moduleInfo.moduleName, - ] - - try runTuistCommand(args) - } - - func registerDependency(_ moduleInfo: ModuleInfo) throws { - let dependencyFilePath = "\(currentPath)Tuist/ProjectDescriptionHelpers/Dependencies/Dependency+Module.swift" - - guard var content = try? String(contentsOfFile: dependencyFilePath, encoding: .utf8) else { - throw ModuleGenerationError.failedToReadDependencyFile - } - - let newDependencies = createDependencyDeclarations(for: moduleInfo) - - guard let range = content.range(of: "public extension TargetDependency.Module.\(moduleInfo.layer.rawValue) {") else { - throw ModuleGenerationError.invalidInsertionPoint - } - - let insertionPoint = content.index(after: range.upperBound) - content.insert(contentsOf: " \(newDependencies.joined(separator: "\n "))\n", at: insertionPoint) - - let formattedContent = formatDependencyExtension(content, for: moduleInfo.layer) - - do { - try formattedContent.write(toFile: dependencyFilePath, atomically: true, encoding: .utf8) - print("✅ Dependency added to Dependency+Module.swift") - } catch { - throw ModuleGenerationError.failedToWriteDependencyFile - } - } - - func createDependencyDeclarations(for moduleInfo: ModuleInfo) -> [String] { - let moduleLowercaseName = moduleInfo.moduleName.prefix(1).lowercased() + moduleInfo.moduleName.dropFirst() - var declarations = [ - "static let \(moduleLowercaseName) = TargetDependency.\(moduleInfo.layer.rawValue.lowercased())(name: \"\(moduleInfo.moduleName)\")", - ] - - if moduleInfo.hasInterface { - declarations.append( - "static let \(moduleLowercaseName)Interface = TargetDependency.\(moduleInfo.layer.rawValue.lowercased())(name: \"\(moduleInfo.moduleName)\", targetType: .interface)" - ) - } - - if moduleInfo.hasTestResources { - declarations.append( - "static let \(moduleLowercaseName)TestResources = TargetDependency.\(moduleInfo.layer.rawValue.lowercased())(name: \"\(moduleInfo.moduleName)\", targetType: .testResources)" - ) - } - - return declarations - } - - /// Formats the dependency extension in the Dependency+Module.swift file. - /// - /// This method is responsible for organizing and formatting the dependency declarations - /// within a specific layer's extension in the Dependency+Module.swift file. It performs - /// the following tasks: - /// - /// 1. Identifies the correct extension block for the given layer. - /// 2. Sorts the dependency declarations alphabetically within the extension. - /// 3. Ensures proper indentation and spacing of the declarations. - func formatDependencyExtension(_ content: String, for layer: LayerType) -> String { - let lines = content.components(separatedBy: .newlines) - var formattedLines: [String] = [] - var inTargetExtension = false - var extensionLines: [String] = [] - var commentLine: String? - var emptyLineAdded = false - - for line in lines { - if line.contains("public extension TargetDependency.Module.\(layer.rawValue)") { - inTargetExtension = true - formattedLines.append(line) - } else if inTargetExtension { - if line.contains("}") { - // Add comment and empty line if they exist - if let comment = commentLine { - formattedLines.append(" " + comment) - } - if !emptyLineAdded { - formattedLines.append("") - } - // Sort and format the extension lines - extensionLines.sort() - formattedLines.append(contentsOf: extensionLines.map { " \($0)" }) - formattedLines.append("}") - inTargetExtension = false - } else if line.trimmingCharacters(in: .whitespaces).starts(with: "//") { - commentLine = line.trimmingCharacters(in: .whitespaces) - } else if line.trimmingCharacters(in: .whitespaces).isEmpty { - emptyLineAdded = true - } else if !line.trimmingCharacters(in: .whitespaces).isEmpty { - // Split multiple declarations on the same line - let declarations = line.components(separatedBy: "static let") - .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } - .map { "static let " + $0.trimmingCharacters(in: .whitespaces) } - extensionLines.append(contentsOf: declarations) - } - } else { - formattedLines.append(line) - } - } - - return formattedLines.joined(separator: "\n") - } - - func listCreatedTargets(_ moduleInfo: ModuleInfo) -> String { - var targets = [moduleInfo.moduleName] - - if moduleInfo.hasInterface { - targets.append("\(moduleInfo.moduleName)Interface") - } - if moduleInfo.hasTestResources { - targets.append("\(moduleInfo.moduleName)TestResources") - } - if moduleInfo.hasUnitTests { - targets.append("\(moduleInfo.moduleName)Tests") - } - if moduleInfo.hasUITests { - targets.append("\(moduleInfo.moduleName)UITests") - } - if moduleInfo.hasExample { - targets.append("\(moduleInfo.moduleName)Example") - } - - return targets.joined(separator: ", ") - } - - func runTuistCommand(_ args: [String]) throws { - let process = Process() - process.launchPath = "/usr/bin/env" - process.arguments = ["tuist"] + args - - let pipe = Pipe() - process.standardOutput = pipe - process.standardError = pipe - - do { - try process.run() - process.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - guard let output = String(data: data, encoding: .utf8) else { - throw ModuleGenerationError.failedToReadTuistOutput - } - print(output) - - if process.terminationStatus != 0 { - throw NSError( - domain: "TuistError", - code: Int(process.terminationStatus), - userInfo: [NSLocalizedDescriptionKey: "Tuist command failed"] - ) - } - } catch { - print("Failed to run Tuist command: \(error)") - throw error - } - } - - func getIntInput(min: Int, max: Int) -> Int { - while true { - print("\u{001B}[1mEnter a number between \(min) and \(max):\u{001B}[0m", terminator: " ") - if let input = readLine(), let number = Int(input), number >= min, number <= max { - return number - } - print("Invalid input. Please try again.") - } - } - - func getBoolInput(_ question: String) -> Bool { - while true { - print("\u{001B}[1m\(question)\u{001B}[0m (y/n):", terminator: " ") - if let input = readLine()?.lowercased() { - switch input { - case "y", "yes": - return true - case "n", "no": - return false - default: - break - } - } - print("Invalid input. Please enter 'y' or 'n'.") - } - } -} - -// swiftlint:enable disable_print - -// MARK: - Main Execution - -let moduleGenerator = ModuleGenerator() -moduleGenerator.generateModule() diff --git a/scripts/git-format-staged b/scripts/git-format-staged deleted file mode 100755 index 6644a57..0000000 --- a/scripts/git-format-staged +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env python3 -# -# Git command to transform staged files according to a command that accepts file -# content on stdin and produces output on stdout. This command is useful in -# combination with `git add -p` which allows you to stage specific changes in -# a file. This command runs a formatter on the file with staged changes while -# ignoring unstaged changes. -# -# Usage: git-format-staged [OPTION]... [FILE]... -# Example: git-format-staged --formatter 'prettier --stdin-filepath "{}"' '*.js' -# -# Tested with Python versions 3.8 - 3.13. -# -# Original author: Jesse Hallett -# -# Source: https://github.com/hallettj/git-format-staged -# Reason for copying this is that the script can be installed using npm but we don't want that - -from __future__ import print_function -import argparse -from fnmatch import fnmatch -from gettext import gettext as _ -import os -import re -import subprocess -import sys - -# The string $VERSION is replaced during the publish process. -VERSION = '3.1.1' -PROG = sys.argv[0] - -def info(msg): - print(msg, file=sys.stdout) - -def warn(msg): - print('{}: warning: {}'.format(PROG, msg), file=sys.stderr) - -def fatal(msg): - print('{}: error: {}'.format(PROG, msg), file=sys.stderr) - exit(1) - -def format_staged_files(file_patterns, formatter, git_root, update_working_tree=True, write=True, verbose=False): - try: - output = subprocess.check_output([ - 'git', 'diff-index', - '--cached', - '--diff-filter=AM', # select only file additions and modifications - '--no-renames', - 'HEAD' - ]) - for line in output.splitlines(): - entry = parse_diff(line.decode('utf-8')) - entry_path = normalize_path(entry['src_path'], relative_to=git_root) - if entry['dst_mode'] == '120000': - # Do not process symlinks - continue - if not (matches_some_path(file_patterns, entry_path)): - continue - if format_file_in_index(formatter, entry, update_working_tree=update_working_tree, write=write, verbose=verbose): - info('Reformatted {} with {}'.format(entry['src_path'], formatter)) - except Exception as err: - fatal(str(err)) - -# Run formatter on file in the git index. Creates a new git object with the -# result, and replaces the content of the file in the index with that object. -# Returns hash of the new object if formatting produced any changes. -def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=True, verbose=False): - orig_hash = diff_entry['dst_hash'] - new_hash = format_object(formatter, orig_hash, diff_entry['src_path'], verbose=verbose) - - # If the new hash is the same then the formatter did not make any changes. - if not write or new_hash == orig_hash: - return None - - # If the content of the new object is empty then the formatter did not - # produce any output. We want to abort instead of replacing the file with an - # empty one. - if object_is_empty(new_hash): - return None - - replace_file_in_index(diff_entry, new_hash) - - if update_working_tree: - try: - patch_working_file(diff_entry['src_path'], orig_hash, new_hash) - except Exception as err: - # Errors patching working tree files are not fatal - warn(str(err)) - - return new_hash - -file_path_placeholder = re.compile(r'\{\}') - -# Run formatter on a git blob identified by its hash. Writes output to a new git -# blob, and returns the hash of the new blob. -def format_object(formatter, object_hash, file_path, verbose=False): - get_content = subprocess.Popen( - ['git', 'cat-file', '-p', object_hash], - stdout=subprocess.PIPE - ) - command = re.sub(file_path_placeholder, file_path, formatter) - if verbose: - info(command) - format_content = subprocess.Popen( - command, - shell=True, - stdin=get_content.stdout, - stdout=subprocess.PIPE - ) - write_object = subprocess.Popen( - ['git', 'hash-object', '-w', '--stdin'], - stdin=format_content.stdout, - stdout=subprocess.PIPE - ) - - get_content.stdout.close() - format_content.stdout.close() - - if get_content.wait() != 0: - raise ValueError('unable to read file content from object database: ' + object_hash) - - if format_content.wait() != 0: - raise Exception('formatter exited with non-zero status') # TODO: capture stderr from format command - - new_hash, err = write_object.communicate() - - if write_object.returncode != 0: - raise Exception('unable to write formatted content to object database') - - return new_hash.decode('utf-8').rstrip() - -def object_is_empty(object_hash): - get_content = subprocess.Popen( - ['git', 'cat-file', '-p', object_hash], - stdout=subprocess.PIPE - ) - content, err = get_content.communicate() - - if get_content.returncode != 0: - raise Exception('unable to verify content of formatted object') - - return not content - -def replace_file_in_index(diff_entry, new_object_hash): - subprocess.check_call(['git', 'update-index', - '--cacheinfo', '{},{},{}'.format( - diff_entry['dst_mode'], - new_object_hash, - diff_entry['src_path'] - )]) - -def patch_working_file(path, orig_object_hash, new_object_hash): - patch = subprocess.check_output( - ['git', 'diff', '--no-ext-diff', '--color=never', orig_object_hash, new_object_hash] - ) - - # Substitute object hashes in patch header with path to working tree file - patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(new_object_hash.encode(), path.encode()) - - apply_patch = subprocess.Popen( - ['git', 'apply', '-'], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - - output, err = apply_patch.communicate(input=patch_b) - - if apply_patch.returncode != 0: - raise Exception('could not apply formatting changes to working tree file {}'.format(path)) - -# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path? -diff_pat = re.compile(r'^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t([^\t]+)(?:\t([^\t]+))?$') - -# Parse output from `git diff-index` -def parse_diff(diff): - m = diff_pat.match(diff) - if not m: - raise ValueError('Failed to parse diff-index line: ' + diff) - return { - 'src_mode': unless_zeroed(m.group(1)), - 'dst_mode': unless_zeroed(m.group(2)), - 'src_hash': unless_zeroed(m.group(3)), - 'dst_hash': unless_zeroed(m.group(4)), - 'status': m.group(5), - 'score': int(m.group(6)) if m.group(6) else None, - 'src_path': m.group(7), - 'dst_path': m.group(8) - } - -zeroed_pat = re.compile(r'^0+$') - -# Returns the argument unless the argument is a string of zeroes, in which case -# returns `None` -def unless_zeroed(s): - return s if not zeroed_pat.match(s) else None - -def get_git_root(): - return subprocess.check_output( - ['git', 'rev-parse', '--show-toplevel'] - ).decode('utf-8').rstrip() - -def normalize_path(p, relative_to=None): - return os.path.abspath( - os.path.join(relative_to, p) if relative_to else p - ) - -def matches_some_path(patterns, target): - is_match = False - for signed_pattern in patterns: - (is_pattern_positive, pattern) = from_signed_pattern(signed_pattern) - if fnmatch(target, normalize_path(pattern)): - is_match = is_pattern_positive - return is_match - -# Checks for a '!' as the first character of a pattern, returns the rest of the -# pattern in a tuple. The tuple takes the form (is_pattern_positive, pattern). -# For example: -# from_signed_pattern('!pat') == (False, 'pat') -# from_signed_pattern('pat') == (True, 'pat') -def from_signed_pattern(pattern): - if pattern[0] == '!': - return (False, pattern[1:]) - else: - return (True, pattern) - -class CustomArgumentParser(argparse.ArgumentParser): - def parse_args(self, args=None, namespace=None): - args, argv = self.parse_known_args(args, namespace) - if argv: - msg = argparse._( - 'unrecognized arguments: %s. Do you need to quote your formatter command?' - ) - self.error(msg % ' '.join(argv)) - return args - -if __name__ == '__main__': - parser = CustomArgumentParser( - description='Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.', - epilog='Example: %(prog)s --formatter "prettier --stdin-filepath \'{}\'" "src/*.js" "test/*.js"' - ) - parser.add_argument( - '--formatter', '-f', - required=True, - help='Shell command to format files, will run once per file. Occurrences of the placeholder `{}` will be replaced with a path to the file being formatted. (Example: "prettier --stdin-filepath \'{}\'")' - ) - parser.add_argument( - '--no-update-working-tree', - action='store_true', - help='By default formatting changes made to staged file content will also be applied to working tree files via a patch. This option disables that behavior, leaving working tree files untouched.' - ) - parser.add_argument( - '--no-write', - action='store_true', - help='Prevents %(prog)s from modifying staged or working tree files. You can use this option to check staged changes with a linter instead of formatting. With this option stdout from the formatter command is ignored. Example: %(prog)s --no-write -f "eslint --stdin --stdin-filename \'{}\' >&2" "*.js"' - ) - parser.add_argument( - '--version', - action='version', - version='%(prog)s version {}'.format(VERSION), - help='Display version of %(prog)s' - ) - parser.add_argument( - '--verbose', - help='Show the formatting commands that are running', - action='store_true' - ) - parser.add_argument( - 'files', - nargs='+', - help='Patterns that specify files to format. The formatter will only transform staged files that are given here. Patterns may be literal file paths, or globs which will be tested against staged file paths using Python\'s fnmatch function. For example "src/*.js" will match all files with a .js extension in src/ and its subdirectories. Patterns may be negated to exclude files using a "!" character. Patterns are evaluated left-to-right. (Example: "main.js" "src/*.js" "test/*.js" "!test/todo/*")' - ) - args = parser.parse_args() - files = vars(args)['files'] - format_staged_files( - file_patterns=files, - formatter=vars(args)['formatter'], - git_root=get_git_root(), - update_working_tree=not vars(args)['no_update_working_tree'], - write=not vars(args)['no_write'], - verbose=vars(args)['verbose'] - ) \ No newline at end of file diff --git a/scripts/rename.sh b/scripts/rename.sh deleted file mode 100755 index 79014fb..0000000 --- a/scripts/rename.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -original=STRVTemplate -# Used for Keychain -keysOriginal=strv_template -updated=`echo $1 | sed 's/ /_/g'` -updated_lowercase=`echo $updated | tr '[:upper:]' '[:lower:]'` - -echo "Starting renaming of STRVTemplate -> $1" - -# Replace strings in all files (except specific directories) -echo "Replacing STRVTemplate in all files..." -find . -type f -not -path "./.git/*" -not -path "./.githooks*" -not -path "./Pods*" -not -path "./scripts*" -not -path "./vendor*" -not -path "./.bundle*" -not -path "./Tuist/.build/*" -print0 | while IFS= read -r -d '' file; do - # First, copy the file with its permissions - cp "$file" "$file.tmp" - sed "s/$original/$updated/g; s/$keysOriginal/$updated_lowercase/g" "$file" 2> /dev/null 1> "$file.tmp" - size=`stat -f%z "$file.tmp"` - if [ "$size" = "0" ]; then - rm "$file.tmp" - else - rm "$file" - mv "$file.tmp" "$file" - fi -done - -# Rename directories -echo "Renaming directories..." -find . -type d -name "*$original*" -print0 | while IFS= read -r -d '' file; do - new=`echo "$file" | sed "s/$original/$updated/g"` - mv "$file" "$new" -done - -# Rename files -echo "Renaming files..." -find . -type f -name "*$original*" -print0 | while IFS= read -r -d '' file; do - new=`echo "$file" | sed "s/$original/$updated/g"` - mv "$file" "$new" -done - -echo "Renaming process completed." \ No newline at end of file diff --git a/scripts/setup_bundler.sh b/scripts/setup_bundler.sh deleted file mode 100755 index f783f37..0000000 --- a/scripts/setup_bundler.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -desired_bundler_version=$(cat .bundler-version) -installed_bundler_version=$(gem list bundler | grep -w $desired_bundler_version) - -if [ -z "$installed_bundler_version" ]; then - echo "Bundler version $desired_bundler_version is not installed. Installing now..." - gem install bundler -v $desired_bundler_version -else - echo "Found correct Bundler version: $desired_bundler_version" -fi \ No newline at end of file diff --git a/scripts/setup_ci.sh b/scripts/setup_ci.sh deleted file mode 100755 index 57535fc..0000000 --- a/scripts/setup_ci.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -if [ "$1" == "enterprise" ] || [ "$1" == "testflight" ] || [ "$1" == "integrations" ]; then - [[ $1 = "integrations" ]] && yml="$1.yml" || yml="$1-deployment.yml" - - mkdir -p .github/workflows - - # Copy environment file if it exists - if [ -f "fastlane/sample.env.$1" ]; then - cp "fastlane/sample.env.$1" "fastlane/.env.$1" - fi - - # Copy workflow file if it exists - if [ -f ".github/sample-workflows/$yml" ]; then - cp ".github/sample-workflows/$yml" ".github/workflows/$yml" - - if [ "$1" != "integrations" ]; then - echo "🟡 Configure .github/workflows/$yml and provide secrets via Github secrets if needed." - echo "If you want to run fastlane locally, make sure that fastlane/.env.$1 is .gitignored and contains all necessary secrets" - fi - else - if [ "$1" == "integrations" ]; then - echo "💡 Integrations workflows are already set up in .github/workflows/" - else - echo "⚠️ Warning: .github/sample-workflows/$yml not found" - fi - fi -else - echo "Provide a CI type: 'enterprise', 'testflight' or 'integrations'." -fi diff --git a/scripts/setup_tuist_githook.sh b/scripts/setup_tuist_githook.sh deleted file mode 100755 index b7c8055..0000000 --- a/scripts/setup_tuist_githook.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Script for generating project files from the Tuist definition files -script=" -# Tuist -if [ -f \"App/Project.swift\" ]; then - tuist generate --no-open -fi" - -# Destination file where the script should be set -hookFile=".githooks/post-checkout" - -if [ ! -f "$hookFile" ]; then - # post-checkout git hook file doesn't exist - create file, add header, and script - echo "#!/bin/bash" > "$hookFile" - echo "$script" >> "$hookFile" -elif ! grep -q "Tuist" "$hookFile"; then - # File exists but the Tuist section is missing - add just the script - echo "$script" >> "$hookFile" -fi - -# Hooks needs to be executable -chmod +x .githooks/post-checkout \ No newline at end of file diff --git a/scripts/swiftformat.sh b/scripts/swiftformat.sh deleted file mode 100755 index a467bb5..0000000 --- a/scripts/swiftformat.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -# Description: -# This script facilitates the running of SwiftFormat. -# It checks for the correct version of SwiftFormat, and runs SwiftFormat on git staged files using 'git-format-staged' binary. -# The desired version of SwiftFormat is specified in a '.swiftformat-version' file located in the project root directory. - -# Usage: -# This script is executed during pre-commit git hook in order to prevent the loss of Xcode's undo functionality - -# Exit Codes: -# - 0: SwiftFormat ran successfully or script exited early in a CI environment. -# - 1: An error occurred, such as SwiftFormat not being installed or version mismatch. - -if [ $CI ]; then - echo "No need to run SwiftFormat on CI" - exit 0 -fi - -cd "$(dirname "$0")/.." - -eval "$($HOME/.local/bin/mise activate -C $SRCROOT bash --shims)" - -swiftformat_bin=$(mise which swiftformat) - -if ! mise which swiftformat >/dev/null; then - echo "error: SwiftFormat is not installed. Install by running setup.swift install or setup.swift install-dependencies" - exit 1 -fi - -desired_version=$(mise current swiftformat) -current_version=$(mise exec -- swiftformat --version) - -if [[ $current_version != $desired_version ]]; then - echo "error: SwiftFormat version mismatch. Expected $desired_version but found $current_version." - echo "error: Install the correct version by running setup.swift install" - exit 1 -fi - -# Run SwiftFormat on git-staged files -cd "$(git rev-parse --show-toplevel)" - -./scripts/git-format-staged --formatter "$swiftformat_bin stdin --stdinpath '{}'" "*.swift" \ No newline at end of file diff --git a/scripts/swiftlint.sh b/scripts/swiftlint.sh deleted file mode 100755 index 3a26f40..0000000 --- a/scripts/swiftlint.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# Description: -# This script facilitates the running of SwiftLint. -# It checks for the correct version of SwiftLint, and runs SwiftLint. -# The desired version of SwiftLint is fetched directly via mise. - -# Usage: -# This script is run as a pre-build phase script. -# In a CI environment, SwiftLint will be run by Danger instead. - -# Exit Codes: -# - 0: SwiftLint ran successfully or script exited early in a CI environment. -# - 1: An error occurred, such as SwiftLint not being installed or version mismatch. - -if [ $CI ]; then - echo "No need to run SwiftLint, Danger will run it!" - exit 0 -fi - -cd "$(dirname "$0")/.." - -# Activate Mise so that Xcode can find it (https://mise.jdx.dev/ide-integration.html#xcode) -eval "$($HOME/.local/bin/mise activate -C $SRCROOT bash --shims)" - -if ! mise which swiftlint >/dev/null; then - echo "error: SwiftLint is not installed. Install by running setup.swift install or setup.swift install-dependencies" - exit 1 -fi - -desired_version=$(mise current swiftlint) -current_version=$(mise exec -- swiftlint --version) - -if [[ $current_version != $desired_version ]]; then - echo "error: SwiftLint version mismatch. Expected $desired_version but found $current_version." - echo "error: Install the correct version by running setup.swift install" - exit 1 -fi - -# Run SwiftLint -mise exec -- swiftlint \ No newline at end of file diff --git a/scripts/update_dependencies.rb b/scripts/update_dependencies.rb deleted file mode 100644 index 84eaa48..0000000 --- a/scripts/update_dependencies.rb +++ /dev/null @@ -1,128 +0,0 @@ -# update_dependencies.rb -# -# Description: -# This script updates the versions of our development tools and dependencies. -# It currently supports SwiftLint, SwiftFormat, Tuist, and Cocoapods if specified in a Gemfile. -# -# Usage: -# This script is run via the `swift setup.swift update-tools` -# Ensure Gemfile is present if updating Cocoapods version. -# -# Note: -# Cocoapods are updated via the `setup.swift`` but Swiftlint + SwiftFormat and deliberately updated -# via their specific scripts to limit the number of requests that need to be done to Github. -# There is a limit rate to unauthorized requests per IP address of 60/hour. - -require 'json' -require 'net/http' -require 'uri' - -# Definitions -def get_current_version(tool_name) - # Fetch current version using mise current - `mise current #{tool_name}`.strip -end - -def get_latest_version(tool_name) - # Fetch latest version using mise latest - `mise latest #{tool_name}`.strip -end - -def success_message(tool, current_version, latest_version) - "🟢 Updated #{tool} from #{current_version} to #{latest_version}" -end - -def no_update_message(tool, current_version) - "🟡 No update needed for #{tool}. Current version matches the latest version: #{current_version}" -end - -def failure_message(tool, error_message) - "🔴 Failed to update #{tool}. #{error_message}" -end - -def gemfile_contains_cocoapods?(gemfile_content) - !!gemfile_content.match(/gem 'cocoapods'/) -end - -def update_gemfile_with_cocoapods_version(cocoapods_version, gemfile_content) - updated_content = gemfile_content.gsub(/gem 'cocoapods', '.*'/, "gem 'cocoapods', '#{cocoapods_version}'") - File.write('Gemfile', updated_content) -end - -def update_cocoapods_version(gemfile_content) - if gemfile_contains_cocoapods?(gemfile_content) - current_cocoapods_version_match = gemfile_content.match(/gem 'cocoapods', '(.*?)'/) - - if current_cocoapods_version_match - current_cocoapods_version = current_cocoapods_version_match[1] - latest_cocoapods_version = `gem info cocoapods --remote | grep '^cocoapods' | awk '{print $2}' | tr -d '()'`.strip - - if latest_cocoapods_version.empty? - puts failure_message('Cocoapods', 'Unable to fetch latest version.') - elsif current_cocoapods_version != latest_cocoapods_version - update_gemfile_with_cocoapods_version(latest_cocoapods_version, gemfile_content) - puts success_message('Cocoapods', current_cocoapods_version, latest_cocoapods_version) - - # Update Cocoapods by installing the bundle - system('swift setup.swift install-bundle') - system('bundle exec pod install') - else - puts no_update_message('Cocoapods', current_cocoapods_version) - end - else - puts 'Error parsing current Cocoapods version in Gemfile.' - end - else - puts "Gemfile doesn't contain Cocoapods" - end -end - -def update_mise_self - puts "Running Mise self-update..." - system('mise self-update') -end - -# Update Mise -update_mise_self - -# Tools -tools = ['swiftlint', 'swiftformat', 'tuist'] - -# Update each of the tools specified above -tools.each do |mise_key| - begin - # Fetch current and latest versions using mise commands - current_version = get_current_version(mise_key) - latest_version = get_latest_version(mise_key) - - if latest_version.empty? - puts failure_message(mise_key.capitalize, 'Unable to fetch the latest version using mise latest.') - next - end - - if current_version != latest_version - # Use mise use to set the tool to the latest version - system("mise use #{mise_key}@#{latest_version}") - - # Success message - puts success_message(mise_key.capitalize, current_version, latest_version) - else - # No update needed message - puts no_update_message(mise_key.capitalize, current_version) - end - rescue StandardError => e - puts failure_message(mise_key.capitalize, e.message) - end -end - -# Update Cocoapods if found in Gemfile -if File.exist?('Gemfile') - begin - gemfile_content = File.read('Gemfile') - update_cocoapods_version(gemfile_content) - rescue StandardError => e - puts failure_message('Cocoapods', e.message) - end -else - puts 'Gemfile does not exist.' -end \ No newline at end of file diff --git a/scripts/verify_ruby.sh b/scripts/verify_ruby.sh deleted file mode 100755 index 24dae9f..0000000 --- a/scripts/verify_ruby.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash - -# Description: -# This script ensures, the recommended Ruby version specified in a .ruby-version file, is installed and active for this repository. -# If the required version is not found, the script attempts to install it using the detected Ruby version manager (rbenv, rvm, or chruby). - -recommended_ruby_version=$(cat .ruby-version) -# Attempt to get the current Ruby version, capturing any error output -current_ruby_version=$(ruby -e 'puts RUBY_VERSION' 2>&1) - -# Function to check which Ruby version manager is in use -detect_ruby_manager() { - if command -v rbenv >/dev/null 2>&1; then - echo "rbenv" - elif command -v rvm >/dev/null 2>&1; then - echo "rvm" - elif command -v chruby >/dev/null 2>&1; then - echo "chruby" - else - echo "none" - fi -} - -install_ruby_version() { - local version=$1 - local manager=$2 - - case $manager in - rbenv) - echo "Attempting to install Ruby $version using rbenv..." - brew update && brew upgrade rbenv ruby-build # make sure that we have latest ruby-build - rbenv install "$version" && rbenv local "$version" - ;; - rvm) - echo "Attempting to install Ruby $version using rvm..." - rvm install "$version" && rvm use "$version" - ;; - chruby) - echo "Attempting to install Ruby $version using ruby-install..." - ruby-install ruby-"$version" && chruby ruby-"$version" - ;; - *) - echo "Unable to install Ruby $version. Unsupported Ruby version manager." - return 1 - ;; - esac -} - -# Function to output a warning message and instructions based on the Ruby manager -output_warning() { - local recommended_ruby_version=$1 - local ruby_manager=$2 - - echo "⚠️ Recommended Ruby version $recommended_ruby_version is not installed." - - # ANSI escape code to start bold and underline text - local bold_underline="\033[1;4m" - - # ANSI escape code to reset text formatting - local reset="\033[0m" - - case $ruby_manager in - rbenv) - echo -e "Please install it using rbenv with the following command:" - echo -e "${bold_underline}rbenv install $recommended_ruby_version${reset}" - ;; - rvm) - echo -e "Please install it using rvm with the following command:" - echo -e "${bold_underline}rvm install $recommended_ruby_version${reset}" - ;; - chruby) - echo -e "Please install it using ruby-install with the following command:" - echo -e "${bold_underline}ruby-install ruby-$recommended_ruby_version${reset}" - ;; - *) - echo "Please install it using your Ruby version manager." - ;; - esac -} - -# If the current Ruby version matches the recommended version, we're done -if [[ "$current_ruby_version" == "$recommended_ruby_version" ]]; then - echo "🟢 Found recommended Ruby version: $recommended_ruby_version" - exit 0 -fi - -ruby_manager=$(detect_ruby_manager) - -# Attempt to install the recommended Ruby version -install_ruby_version "$recommended_ruby_version" "$ruby_manager" -install_result=$? - -# Check the Ruby version again after attempting to install -current_ruby_version=$(ruby -e 'puts RUBY_VERSION' 2>&1) - -if [[ "$current_ruby_version" == "$recommended_ruby_version" ]]; then - echo "🟢 Successfully installed and switched to recommended Ruby version: $recommended_ruby_version" - exit 0 -elif [[ $install_result -eq 0 ]]; then - echo "⚠️ Installed Ruby $recommended_ruby_version, but failed to switch to it. Please check your Ruby version manager." - exit 1 -else - output_warning "$recommended_ruby_version" "$ruby_manager" - exit 1 -fi \ No newline at end of file diff --git a/setup.swift b/setup.swift deleted file mode 100755 index da4225e..0000000 --- a/setup.swift +++ /dev/null @@ -1,761 +0,0 @@ -#!/usr/bin/env swift -import Foundation - -// swiftlint:disable all - -let shell = Shell() -// This enables the script to exit when the user presses Ctrl+C -signal(SIGINT) { _ in - Task { @MainActor in - shell.terminateCurrentTask() - exit(0) - } -} - -enum Command: String, CaseIterable { - case help - case setup - case install - case installBundle = "install-bundle" - case installDependencies = "install-dependencies" - case updateDependencies = "update-dependencies" - case setupTuist = "setup-tuist" - case setupHooks = "setup-hooks" - case renameProject = "rename-project" - case setupCI = "setup-ci" - case removeTuistFiles = "remove-tuist-files" - case setupVersionIcon = "install-version-icon" -} - -extension Command { - var description: String { - switch self { - case .help: - "Prints this manual." - - case .setup: - "Runs the following commands: setupCI -> renameProject -> setupHooks -> installMiseDependencies -> installBundle -> setupTuist (if needed) -> setupVersionIcon." - - case .install: - "Runs the following commands: setupHooks -> installMiseDependencies -> installBundle -> installTuistDependencies -> installVersionIcon (if needed)." - - case .installBundle: - "We use Gemfile to ensure that you have all dependencies in correct versions. `install-bundle` installs all dependencies to the project directory." - - case .installDependencies: - "Installs Mise and Tuist dependencies (Tuist, SwiftLint, SwiftFormat) if needed." - - case .updateDependencies: - "Updates dependencies (Tuist, SwiftLint, SwiftFormat) to the latest versions" - - case .setupTuist: - "Installs Tuist environment, installs dependencies and generates the project files, adds appropriate files to the gitignore, removes gitignored files from git and setups git hooks." - - case .setupHooks: - "Creates symbolic links for hooks in githooks to the .git/hooks dictionary." - - case .renameProject: - "If you want to rename the project from `STRVTemplate` to something different (and you probably want that), just run the following command. You'll be asked for the name." - - case .setupCI: - "Copies .env and workflow files as needed." - - case .removeTuistFiles: - "Removes Tuist definition files." - - case .setupVersionIcon: - "VersionIcon is optional feature. It creates an app icon overlay with the ribbon and/or version (build) text based on the app configuration. VersionIcon is added as script build phase. It is STRV internal project and it is highly customizable." - } - } -} - -enum SetupError: LocalizedError { - case fileOperationFailed(String? = nil) - case emptyProjectName - case missingWorkspace - case miseNotInstalled(command: String) - case miseNotActivated(command: String) - - var errorDescription: String? { - switch self { - case let .fileOperationFailed(error?): - "file operation failed: \(error)" - case .fileOperationFailed(.none): - "file operation failed" - case .emptyProjectName: - "project name can't be empty" - case .missingWorkspace: - "no workspace in current directory" - case let .miseNotInstalled(command): - "Please install Mise (https://mise.jdx.dev/getting-started.html#_1-install-mise-cli) and run the `\(command)` command again." - case let .miseNotActivated(command): - "Please activate Mise (https://mise.jdx.dev/getting-started.html#_2a-activate-mise) and run the `\(command)` command again." - } - } -} - -@MainActor -final class Main { - private let commandRunner = CommandRunner() - - func run() async { - let argumentCommand = CommandLine.argc > 1 ? CommandLine.arguments[1] : nil - - if let argumentCommand { - if let command = Command(rawValue: argumentCommand) { - await commandRunner.run(command) - } else { - print("⚠️ Error: unknown command") - exit(1) - } - } else { - await askForCommand() - } - } -} - -private extension Main { - func askForCommand() async { - printCommands() - print("\n") - if let commandNumber = askForCommandNumber(), - Command.allCases.indices.contains(commandNumber) - { - let command = Command.allCases[commandNumber] - print(" Next time you can do this by directly typing `./setup.swift \(command.rawValue)`") - await commandRunner.run(command) - } else { - print("⚠️ Error: unknown command") - exit(1) - } - } - - func printCommands() { - for (index, command) in Command.allCases.enumerated() { - print("\(index): \(command.rawValue)") - } - } - - func askForCommandNumber() -> Int? { - print("\n💬 Which number would you like to run?") - return readLine().map(Int.init) ?? nil - } -} - -@MainActor -final class CommandRunner { - func run(_ command: Command) async { - do { - switch command { - case .help: - printManual() - - case .installBundle: - try await installBundle(shouldVerifyMise: true) - - case .installDependencies: - let tuistDefinitionFileExists = await tuistDefinitionFileExists() - try await installMiseDependencies(shouldVerifyMise: true) - try await installTuistDependencies(isUsingTuist: tuistDefinitionFileExists) - - case .updateDependencies: - await updateDependencies() - - case .setupTuist: - try await verifyMiseIsInstalled(command: command) - try await setupTuist() - - case .setupHooks: - try setupHooks() - - case .renameProject: - try await renameProject() - - case .setupCI: - await setupCI() - - case .setup: - try await setup() - - case .install: - try await install() - - case .removeTuistFiles: - await removeTuistFiles() - - case .setupVersionIcon: - let tuistDefinitionFileExists = await tuistDefinitionFileExists() - if tuistDefinitionFileExists { - try? await setupVersionIcon(projectName: nil) - try await installTuistDependencies(isUsingTuist: tuistDefinitionFileExists) - } else { - print("⚠️ Error: VersionIcon requires Tuist and it is not installed. Please run `./setup.swift setup-tuist` first.") - exit(1) - } - } - } catch { - print("⚠️ Error: \(error.localizedDescription)") - exit(1) - } - } -} - -private extension CommandRunner { - var isCI: Bool { - guard let ciValue = ProcessInfo.processInfo.environment["CI"] else { - return false - } - return ciValue == "true" || ciValue == "1" - } - - var homeDirectory: String { - FileManager.default.homeDirectoryForCurrentUser.path - } - - func printManual() { - print("\n🔵 Available commands:\n") - for (index, command) in Command.allCases.enumerated() { - print("\(index): \(command.rawValue)") - print("// \(command.description)\n") - } - } - - func setup() async throws { - try await verifyMiseIsInstalled(command: .setup) - await setupCI(offerSkip: true) - let projectName = try await renameProject() - try setupHooks() - try await installMiseDependencies(shouldVerifyMise: false) - try await installBundle(shouldVerifyMise: false) - try await setupVersionIcon(projectName: projectName) - try await setupTuist() - } - - func install() async throws { - try await verifyMiseIsInstalled(command: .install) - try setupHooks() - let tuistDefinitionFileExists = await tuistDefinitionFileExists() - try await installMiseDependencies(shouldVerifyMise: false) - try await installBundle(shouldVerifyMise: false) - try await installTuistDependencies(isUsingTuist: tuistDefinitionFileExists) - } - - func installBundle(shouldVerifyMise: Bool) async throws { - if shouldVerifyMise { - try await verifyMiseIsInstalled(command: .installBundle) - } - - print("🔵 Install bundle: \(bundlerVersion)") - await shell.runAndExitIfFailure("./scripts/setup_bundler.sh") - await shell.runAndExitIfFailure("bundle _\(bundlerVersion)_ install") - } - - func installMiseDependencies(shouldVerifyMise: Bool) async throws { - if shouldVerifyMise { - try await verifyMiseIsInstalled(command: .installDependencies) - } - - print("🔵 Install Mise dependencies") - - await shell.runAndExitIfFailure("mise install -y") - } - - func installTuistDependencies(isUsingTuist: Bool) async throws { - print("🔵 Install Tuist dependencies") - - if isUsingTuist { - // It seems we need to use `mise exec` because after the initial Mise installation the $PATH doesn't correctly update - // not even via calling `eval "$(mise activate zsh --shims)"` so Mise is unable to find the correct tool (in this case tuist) - // Every other setup run works correctly without calling `mise exec` because the $PATH updating is the working fine. - // But for the initial "edge-case" it seems we need to keep the `mise exec`. - await shell.runAndExitIfFailure("mise exec -- tuist install") - await shell.runAndExitIfFailure("mise exec -- tuist generate --no-open") - } - } - - func updateDependencies() async { - await shell.runAndExitIfFailure("ruby scripts/update_dependencies.rb") - } - - func setupTuist() async throws { - print("🔵 Setup Tuist") - guard await tuistDefinitionFileExists() else { - print("Tuist Project.swift definition file is missing.") - return - } - try await installTuistDependencies(isUsingTuist: true) - - await shell.runAndExitIfFailure("./scripts/setup_tuist_githook.sh") - try setupHooks() - } - - func setupHooks() throws { - guard !isCI else { - return - } - - print("🔵 Setup hooks") - try installHooks() - } - - func installHooks() throws { - let fileManager = FileManager.default - let currentDirectoryPath = fileManager.currentDirectoryPath - let hooksDirectoryPath = currentDirectoryPath + "/.git/hooks" - let sourceDirectoryPath = currentDirectoryPath + "/.githooks" - - if !fileManager.fileExists(atPath: hooksDirectoryPath) { - print("🟡 Creating missing .git/hooks/ directory") - try fileManager.createDirectory(atPath: hooksDirectoryPath, withIntermediateDirectories: true, attributes: nil) - } - - let hooks = try fileManager.contentsOfDirectory(atPath: sourceDirectoryPath) - .filter { !$0.hasPrefix(".") && URL(fileURLWithPath: $0).pathExtension.isEmpty } - - for hook in hooks { - let target = currentDirectoryPath + "/.git/hooks/\(hook)" - let source = currentDirectoryPath + "/.githooks/\(hook)" - let backup = "\(target).bak" - - guard fileManager.fileExists(atPath: source) else { - print("🟡 Skipping \(hook) hook because it doesn't exist at expected path: \(source)") - continue - } - - if - let destination = try? fileManager.destinationOfSymbolicLink(atPath: target), - destination == source - { - print("🟢 \(hook) hook is already installed") - continue - } - - if fileManager.contentsEqual(atPath: target, andPath: source) { - print("🟢 \(hook) hook is already installed") - continue - } - - if fileManager.fileExists(atPath: target) { - print("🟡 Found an existing hook for \(hook). Creating a backup at \(backup)") - do { - if fileManager.fileExists(atPath: backup) { - try fileManager.removeItem(atPath: backup) - } - try fileManager.moveItem(atPath: target, toPath: backup) - } catch { - throw SetupError.fileOperationFailed("Backup creation failed: \(error.localizedDescription)") - } - } - - do { - try fileManager.createSymbolicLink(atPath: target, withDestinationPath: source) - } catch { - throw SetupError.fileOperationFailed("\(hook) Symlink creation failed: \(error.localizedDescription)") - } - print("🟢 Installed \(hook) hook") - } - } - - @discardableResult - func renameProject() async throws -> String { - print("\n🔵 Rename project") - print("💬 New project name:") - guard let projectName = readLine(), !projectName.isEmpty else { - throw SetupError.emptyProjectName - } - print("🔵 Renaming project... (can take a minute)") - await shell.runAndExitIfFailure("./scripts/rename.sh \"\(projectName)\"") - print("🟢 Renamed to \(projectName)") - return projectName - } - - func setupCI(offerSkip: Bool = false) async { - print("🔵 Setup CI") - - print("🔵 Setting up default Integrations workflow") - await shell.runAndExitIfFailure("./scripts/setup_ci.sh integrations") - - let availableWorkflows = [ - "testflight", - "enterprise", - ] - let workflowsString = availableWorkflows - .enumerated() - .map { "\($0): \($1)" } - .joined(separator: " | ") - print("\n🔵 Available CI workflows: \(workflowsString)") - print("💬 Which workflow number would you like to select?" + (offerSkip ? " Just press enter to skip." : "")) - if let workflowNumber = readLine().map(Int.init) ?? nil, - availableWorkflows.indices.contains(workflowNumber) - { - let selectedWorkflow = availableWorkflows[workflowNumber] - await shell.runAndExitIfFailure("./scripts/setup_ci.sh \"\(selectedWorkflow)\"") - } else { - print("🔵 Skipped") - } - } - - func removeTuistFiles() async { - print("🔵 Remove Tuist files") - await shell.runAndExitIfFailure("rm -rf Tuist App/Project.swift Workspace.swift .tuist-version .githooks/post-checkout .package.resolved Derived") - } - - func tuistDefinitionFileExists() async -> Bool { - await shell.run("test -e App/Project.swift") == 0 - } - - func shouldSetupVersionIcon() -> Bool { - print("\n💬 Do you want to setup VersionIcon script (https://github.com/strvcom/ios-version-icon) ? (y/n)") - let answer = readLine() - if answer == "y" { - return true - } else if answer == "n" { - return false - } else { - print("⚠️ Unknown answer") - exit(1) - } - } - - func setupVersionIcon(projectName: String?) async throws { - // Test whether Tuist is installed - var isdirectory : ObjCBool = true - guard FileManager.default.fileExists(atPath: "Tuist", isDirectory: &isdirectory) else { - print("⚠️ Tuist is not installed") - return - } - - guard shouldSetupVersionIcon() else { - return - } - - var usedProjectName: String - if let projectName { - usedProjectName = projectName - } else { - let currentDirectory = FileManager.default.currentDirectoryPath - let enumerator = FileManager.default.enumerator(atPath: currentDirectory) - let filePaths = enumerator?.allObjects as! [String] - let workspaces = filePaths.filter { $0.contains(".xcworkspace") } - - usedProjectName = try getProjectName(from: workspaces) - } - try setupVersionIconGlobalConstants() - try setupVersionIconUpdateGitIgnore(projectName: usedProjectName) - await setupVersionIconRemoveIconsFromGit(projectName: usedProjectName) - } - - /// Gets the name for project - from the workspace file or by asking the user - func getProjectName(from workspaces: [String]) throws -> String { - // Handle the case that there are many workspace files in the folder - if workspaces.count > 1 { - // Ask the user - print("\n💬 Project name: ") - guard let projectName = readLine(), !projectName.isEmpty else { - throw SetupError.emptyProjectName - } - return projectName - } - // Otherwise derive the project name from single workspace file - else { - guard let workspace = workspaces.first else { - throw SetupError.missingWorkspace - } - - return workspace.replacingOccurrences(of: ".xcworkspace", with: "") - } - } - - /// Change Tuist GlobalConstants.swift - func setupVersionIconGlobalConstants() throws { - print("🔵 Modifying Tuist GlobalConstants") - var globalConstantsContents = try String(contentsOfFile: globalConstantsPath, encoding: .utf8) - globalConstantsContents = globalConstantsContents.replacing("useVersionIcon = false", with: "useVersionIcon = true") - try globalConstantsContents.write(toFile: globalConstantsPath, atomically: true, encoding: .utf8) - } - - /// Update .gitignore for VersionIcon - func setupVersionIconUpdateGitIgnore(projectName: String) throws { - print("🔵 Updating .gitignore") - let gitIgnore = ".gitignore" - var gitIgnoreContents = try String(contentsOfFile: gitIgnore) - - if gitIgnoreContents.contains("AppIcon.appiconset") { - print("VersionIcon is already set in .gitignore") - } else { - gitIgnoreContents.append( - """ - - # VersionIcon - - App/{{AppName}}/Application/Resources/Assets.xcassets/AppIcon.appiconset/* - """ - .replacing("{{AppName}}", with: projectName) - ) - try gitIgnoreContents.write(toFile: gitIgnore, atomically: true, encoding: .utf8) - } - } - - /// Remove icon files from git. We want to avoid commiting the changed icons with every commit. - /// AppIcon is now cosidered as only local and AppIconOriginal is shared image source. - func setupVersionIconRemoveIconsFromGit(projectName: String) async { - print("🔵 Removing AppIcon images from git") - await shell.runAndExitIfFailure("git rm -f \"App/\(projectName)/Application/Resources/Assets.xcassets/AppIcon.appiconset/*\"") - } - - /// Installs VersionIcon if needed. - /// `AppIcon` is git ignored, but Xcode still needs it to build the app. - /// We copy the original icon to `AppIcon.appiconset` if it doesn't exist. - /// This usually happens when existing project is cloned and the icon is not present. - func installVersionIconIfNeeded() throws { - guard try isVersionIconSetup() else { - print("🔵 VersionIcon is not set up. Skipping.") - return - } - - guard let projectName = try getGlobalConstantsProjectName() else { - print("🔴 Could not determine project name from GlobalConstants.swift. Please ensure it is set correctly.") - return - } - let originalAppIconPath = "App/\(projectName)/Application/Resources/Assets.xcassets/AppIconOriginal.appiconset" - let appIconPath = "App/\(projectName)/Application/Resources/Assets.xcassets/AppIcon.appiconset" - - guard FileManager.default.fileExists(atPath: originalAppIconPath) else { - print("🔴 Original AppIcon not found at \(originalAppIconPath). Please ensure it exists.") - return - } - - guard !FileManager.default.fileExists(atPath: appIconPath) else { - print("🟢 AppIcon is already set up.") - return - } - - try FileManager.default.copyItem(atPath: originalAppIconPath, toPath: appIconPath) - print("🟢 Copied original AppIcon to \(appIconPath)") - } - - func isVersionIconSetup() throws -> Bool { - let contents = try String(contentsOfFile: globalConstantsPath, encoding: .utf8) - return contents.contains("useVersionIcon = true") - } - - var bundlerVersion: String { - let fileName = ".bundler-version" - let fileURL = URL(filePath: fileName) - do { - return try String(contentsOf: fileURL, encoding: .utf8) - } catch { - print("⚠️ Error: \(String(describing: error))") - exit(1) - } - } - - func getGlobalConstantsProjectName() throws -> String? { - let globalConstantsContents = try String(contentsOfFile: globalConstantsPath, encoding: .utf8) - let pattern = #".*projectName\s*=\s*"([^"]+)""# - let regex = try NSRegularExpression(pattern: pattern, options: []) - - if let match = regex.firstMatch(in: globalConstantsContents, options: [], range: NSRange(globalConstantsContents.startIndex..., in: globalConstantsContents)), - let range = Range(match.range(at: 1), in: globalConstantsContents) - { - let projectName = String(globalConstantsContents[range]) - return projectName - } - - return nil - } - - var globalConstantsPath: String { - "./Tuist/ProjectDescriptionHelpers/GlobalConstants.swift" - } - - func promptWithDefault(message: String, defaultValue: String = "y") -> String { - print("\(message) [Y/n]: ", terminator: "") - - let response = readLine()?.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) - if let response, !response.isEmpty { - return response - } - return defaultValue - } -} - -// MARK: - Mise setup - -private extension CommandRunner { - /// Checks if Mise is installed, verifies if activated correctly, and handles installation + activation if necessary. - func verifyMiseIsInstalled(command: Command) async throws { - // If Mise is installed - if await shell.run("which -s mise", printOutput: false) == 0 { - print("🔵 Mise is installed.") - } else { - // Check whether it is installed in the expected path - if FileManager.default.fileExists(atPath: "\(homeDirectory)/.local/bin/mise") { - print("🟡 Mise is installed, but not activated.") - let isZshrcUpdated = await verifyAndUpdateZshrc(command: command) - if !isZshrcUpdated { - throw SetupError.miseNotActivated(command: command.rawValue) - } else { - // Force the terminal restart - exit(0) - } - } else { - let install = promptWithDefault(message: "🔴 Mise is not installed and it's required. Would you like to install it now?") - if install == "y" { - try await installMise(command: command) - } else { - throw SetupError.miseNotInstalled(command: command.rawValue) - } - } - } - } - - /// Installs Mise using a shell script and updates .zshrc to activate it. - func installMise(command: Command) async throws { - print("🔵 Installing Mise...") - await shell.runAndExitIfFailure("curl https://mise.jdx.dev/install.sh | sh", printOutput: false) - _ = await verifyAndUpdateZshrc(command: command) - exit(0) - } - - /// Verifies if the Mise activation command is present in .zshrc and prompts to add it if missing. - /// Returns: True/false whether the zshrc was successfully updated - func verifyAndUpdateZshrc(command: Command) async -> Bool { - let zshrcPath = "\(homeDirectory)/.zshrc" - let activationCommand = "eval \"$(\(homeDirectory)/.local/bin/mise activate zsh)\"" - - do { - let zshrcContents = try String(contentsOfFile: zshrcPath, encoding: .utf8) - - if !zshrcContents.contains(activationCommand) { - let response = promptWithDefault(message: "🔴 Mise activation command is not in `~/.zshrc`. Would you like to add it now?") - if response == "y" { - appendToZshrc(command: activationCommand) - print("🟡 In order to finish the Mise install, please restart your Terminal and then run `./setup.swift \(command.rawValue)` again.") - return true - } else { - printManualZshrcUpdateInstructions(command: command) - return false - } - } else { - if !isPathUpdated() { - print("🟡 Mise is activated in ~/.zshrc, but the current Terminal session does not recognize it. Please restart your Terminal and then run `./setup.swift \(command.rawValue)` again.") - exit(0) - } - } - } catch { - print("Failed to read ~/.zshrc: \(error)") - } - return true - } - - func printManualZshrcUpdateInstructions(command: Command) { - print("Please manually add the following line to your .zshrc to activate Mise:") - print("eval \"$(\(homeDirectory)/.local/bin/mise activate zsh)\"") - print("After updating, please restart your Terminal and run the run `./setup.swift \(command.rawValue)` again.") - } - - /// Appends a given command to the user's `.zshrc` file. - func appendToZshrc(command: String) { - let zshrcPath = "\(homeDirectory)/.zshrc" - let fullCommand = "\n\n# Mise \n\(command)\n" - - do { - // Check if .zshrc exists and append the command - if FileManager.default.fileExists(atPath: zshrcPath) { - let fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: zshrcPath)) - fileHandle.seekToEndOfFile() - if let data = fullCommand.data(using: .utf8) { - fileHandle.write(data) - fileHandle.closeFile() - print("🟢 ~/.zshrc was adjusted successfully.") - } - } else { - // If .zshrc does not exist, create it and write the command - try fullCommand.write(to: URL(fileURLWithPath: zshrcPath), atomically: true, encoding: .utf8) - print(".zshrc file created and command added.") - } - } catch { - print("An error occurred: \(error)") - } - } - - func isPathUpdated() -> Bool { - let misePath = "\(homeDirectory)/.local/bin" - let pathEnvironmentVariable = ProcessInfo.processInfo.environment["PATH"] ?? "" - return pathEnvironmentVariable.split(separator: ":").contains { String($0) == misePath } - } -} - -@MainActor -final class Shell { - private var currentTask: Process? - - // credit: https://stackoverflow.com/a/38088651 - // Shell with continuous printing to the console - // - Returns: Shell exit code - @discardableResult - func run(_ command: String, printOutput: Bool = true) async -> Int32 { - await withCheckedContinuation { continuation in - let task = Process() - currentTask = task - - let outpipe = Pipe() - task.standardOutput = outpipe - task.arguments = ["-c", command] - task.executableURL = URL(fileURLWithPath: "/bin/zsh") - task.standardInput = nil - - task.terminationHandler = { result in - DispatchQueue.main.async { - if self.currentTask === task { - self.currentTask = nil - } - continuation.resume(returning: result.terminationStatus) - } - } - - let outputHandle = outpipe.fileHandleForReading - outputHandle.readabilityHandler = { pipe in - guard printOutput else { return } - if let line = String(data: pipe.availableData, encoding: .utf8) { - print(line, terminator: "") - } else { - print("⚠️ Error decoding data: \(pipe.availableData)") - } - } - - try? task.run() - } - } - - func terminateCurrentTask() { - guard let task = currentTask else { - return - } - task.terminate() - } - - func runAndExitIfFailure(_ command: String, printOutput: Bool = true) async { - let exitCode = await run(command, printOutput: printOutput) - if exitCode != 0 { - print("❌ Command failed with exit code \(exitCode)") - exit(1) - } - } - - /// Shell with printing to the console when command terminates. - /// Allowing prompt asking for the user password. - func runWithPrivileges(_ command: String) async { - let myAppleScript = "do shell script \"\(command)\"" - var error: NSDictionary? - let scriptObject = NSAppleScript(source: myAppleScript)! - let output = scriptObject.executeAndReturnError(&error) - let ouputWithReplacedNewLines = (output.stringValue ?? "").replacingOccurrences(of: "\r\n", with: "\n") - print(ouputWithReplacedNewLines) - } -} - -await Main().run() - -// swiftlint:enable all From 87f3458d9ea082b06acf07680849f54dbb82ea09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 14:39:59 +0100 Subject: [PATCH 43/52] remove: scripts --- .bundle/config | 2 -- .bundler-version | 1 - .github/workflows/pr-integrations.yml | 16 ---------------- .mise.toml | 9 --------- 4 files changed, 28 deletions(-) delete mode 100644 .bundle/config delete mode 100644 .bundler-version delete mode 100644 .mise.toml diff --git a/.bundle/config b/.bundle/config deleted file mode 100644 index 2369228..0000000 --- a/.bundle/config +++ /dev/null @@ -1,2 +0,0 @@ ---- -BUNDLE_PATH: "vendor/bundle" diff --git a/.bundler-version b/.bundler-version deleted file mode 100644 index 6ab1bff..0000000 --- a/.bundler-version +++ /dev/null @@ -1 +0,0 @@ -2.5.17 \ No newline at end of file diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml index 2b77175..69bd181 100644 --- a/.github/workflows/pr-integrations.yml +++ b/.github/workflows/pr-integrations.yml @@ -83,22 +83,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Istall Mise (Dev env setup tool) - - name: Install Mise - uses: jdx/mise-action@v2 - with: - install: true - cache: false # change to true when running on macos-latest instead of self-hosted - experimental: true - - # Cache dependencies - - name: Cache Dependencies - uses: ./.github/actions/cache-dependencies - - # Install dependencies (run install script) - - name: Install Dependencies - run: ./setup.swift install - # Run Unit Tests - name: Run Unit Tests run: swift test --enable-code-coverage diff --git a/.mise.toml b/.mise.toml deleted file mode 100644 index e56e919..0000000 --- a/.mise.toml +++ /dev/null @@ -1,9 +0,0 @@ -[tools] -ruby = "3.4.6" -swiftlint = "0.57.0" -swiftformat = "0.54.5" -tuist = "4.65.6" - -[settings] -legacy_version_file = false -experimental = true From 5af88562989c2efc160164b64aadddc19b7286f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 14:43:56 +0100 Subject: [PATCH 44/52] fix: remove setup.swift --- .github/workflows/pr-integrations.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml index 69bd181..b376d45 100644 --- a/.github/workflows/pr-integrations.yml +++ b/.github/workflows/pr-integrations.yml @@ -57,10 +57,6 @@ jobs: - name: Cache Dependencies uses: ./.github/actions/cache-dependencies - # Install dependencies (run install script) - - name: Install Dependencies - run: ./setup.swift install - # uncomment if you are using SwiftLint via SPM plugin # Set SwiftLint version for Danger # - name: Set SwiftLint version From 7b17bd927bd906025d0d973cd24f4478d8918e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 14:55:25 +0100 Subject: [PATCH 45/52] fix: ci --- .github/actions/cache-dependencies/action.yml | 14 - .github/workflows/pr-integrations.yml | 13 +- Gemfile | 14 - Gemfile.lock | 324 ------------------ 4 files changed, 4 insertions(+), 361 deletions(-) delete mode 100644 .github/actions/cache-dependencies/action.yml delete mode 100644 Gemfile delete mode 100644 Gemfile.lock diff --git a/.github/actions/cache-dependencies/action.yml b/.github/actions/cache-dependencies/action.yml deleted file mode 100644 index bc731c0..0000000 --- a/.github/actions/cache-dependencies/action.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Create iOS simulator -description: Create simulator to be used for a test run -runs: - using: composite - steps: - - name: Gem Cache - uses: actions/cache@v4 - id: dependencies_cache - with: - path: | - vendor - key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-gems- diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml index b376d45..d80bcea 100644 --- a/.github/workflows/pr-integrations.yml +++ b/.github/workflows/pr-integrations.yml @@ -53,18 +53,13 @@ jobs: cache: false # change to true when running on macos-latest instead of self-hosted experimental: true - # Cache Dependencies - - name: Cache Dependencies - uses: ./.github/actions/cache-dependencies - - # uncomment if you are using SwiftLint via SPM plugin - # Set SwiftLint version for Danger - # - name: Set SwiftLint version - # run: echo "SWIFTLINT_VERSION=$(cat ./.swiftlint-version)" >> $GITHUB_ENV + # Install Danger + - name: Install Danger + run: gem install danger --no-document # Run Danger - name: Run Danger - run: bundle exec danger --fail-on-errors=true + run: danger --fail-on-errors=true env: GITHUB_TOKEN: ${{ github.token }} diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 3b313e2..0000000 --- a/Gemfile +++ /dev/null @@ -1,14 +0,0 @@ -# STRV s.r.o. 2019 -# STRV - -source 'https://rubygems.org' - -# Ruby 3.4+ compatibility -gem 'abbrev' -gem 'nkf' - -gem 'danger', '9.5.3' -gem 'danger-duplicate_localizable_strings', git: 'https://github.com/strvcom/ios_danger-duplicate_localizable_strings', ref: 'master' -gem 'danger-swiftlint', '0.37.2' -gem 'danger-swiftformat', git: 'https://github.com/strvcom/ios-danger-ruby-swiftformat.git', ref: 'master' - diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index e2770e1..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,324 +0,0 @@ -GIT - remote: https://github.com/strvcom/ios-danger-ruby-swiftformat.git - revision: 3183ee3b72cf7884d046eb1827e3527dd9b0fbb0 - ref: master - specs: - danger-swiftformat (0.9.0) - danger-plugin-api (~> 1.0) - -GIT - remote: https://github.com/strvcom/ios_danger-duplicate_localizable_strings - revision: 4e8153f4a3e03f441230657469ca1609c48fdb75 - ref: master - specs: - danger-duplicate_localizable_strings (0.4.1) - danger (~> 9.0) - -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.8) - abbrev (0.1.2) - activesupport (8.1.2) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - json - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - addressable (2.8.9) - public_suffix (>= 2.0.2, < 8.0) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.4.0) - aws-partitions (1.1220.0) - aws-sdk-core (3.242.0) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - base64 - bigdecimal - jmespath (~> 1, >= 1.6.1) - logger - aws-sdk-kms (1.122.0) - aws-sdk-core (~> 3, >= 3.241.4) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.213.0) - aws-sdk-core (~> 3, >= 3.241.4) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.12.1) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - base64 (0.3.0) - bigdecimal (4.0.1) - 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) - concurrent-ruby (1.3.6) - connection_pool (3.0.2) - cork (0.3.0) - colored2 (~> 3.1) - danger (9.5.3) - base64 (~> 0.2) - claide (~> 1.0) - claide-plugins (>= 0.9.2) - colored2 (>= 3.1, < 5) - cork (~> 0.1) - faraday (>= 0.9.0, < 3.0) - faraday-http-cache (~> 2.0) - git (>= 1.13, < 3.0) - kramdown (>= 2.5.1, < 3.0) - kramdown-parser-gfm (~> 1.0) - octokit (>= 4.0) - pstore (~> 0.1) - terminal-table (>= 1, < 5) - danger-plugin-api (1.0.0) - danger (> 2.0) - danger-swiftlint (0.37.2) - danger - rake (> 10) - thor (~> 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) - drb (2.2.3) - emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.5) - 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.8) - faraday (>= 0.8.0) - http-cookie (>= 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.1) - faraday-excon (1.1.0) - faraday-http-cache (2.6.1) - faraday (>= 0.8) - faraday-httpclient (1.0.1) - faraday-multipart (1.2.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.228.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 (~> 1.2) - 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) - fastlane-sirp (>= 1.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) - 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, < 1.0.0) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) - 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.4.1) - xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-sirp (1.0.0) - sysrandom (~> 1.0) - gh_inspector (1.1.3) - git (2.3.3) - activesupport (>= 5.0) - addressable (~> 2.8) - process_executer (~> 1.1) - rchardet (~> 1.8) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.8.0) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.5.0) - google-cloud-storage (1.47.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - 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.9.0) - mutex_m - i18n (1.14.8) - concurrent-ruby (~> 1.0) - jmespath (1.6.2) - json (2.18.1) - jwt (2.10.2) - base64 - kramdown (2.5.2) - rexml (>= 3.4.4) - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - logger (1.7.0) - mini_magick (4.13.2) - mini_mime (1.1.5) - minitest (6.0.2) - drb (~> 2.0) - prism (~> 1.5) - multi_json (1.19.1) - multipart-post (2.4.1) - mutex_m (0.3.0) - nanaimo (0.4.0) - nap (1.1.0) - naturally (2.3.0) - nkf (0.2.0) - octokit (10.0.0) - faraday (>= 1, < 3) - sawyer (~> 0.9) - open4 (1.3.4) - optparse (0.8.1) - os (1.1.4) - plist (3.7.2) - prism (1.9.0) - process_executer (1.3.0) - pstore (0.2.1) - public_suffix (7.0.2) - rake (13.3.1) - rchardet (1.10.0) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.2.1) - rexml (3.4.4) - rouge (3.28.0) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - sawyer (0.9.3) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - securerandom (0.4.1) - security (0.1.5) - signet (0.21.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - sysrandom (1.0.5) - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - thor (1.5.0) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uber (0.1.0) - unicode-display_width (2.6.0) - uri (1.1.1) - 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.4.1) - rouge (~> 3.28.0) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - arm64-darwin-24 - ruby - -DEPENDENCIES - abbrev - danger (= 9.5.3) - danger-duplicate_localizable_strings! - danger-swiftformat! - danger-swiftlint (= 0.37.2) - fastlane (= 2.228.0) - nkf - -BUNDLED WITH - 2.5.17 From 6e5995138713a1a9b1fc7fa84193c30093753218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 15:05:11 +0100 Subject: [PATCH 46/52] fix: test --- .github/workflows/pr-integrations.yml | 4 +++- Gemfile | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Gemfile diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml index d80bcea..3f2cfd3 100644 --- a/.github/workflows/pr-integrations.yml +++ b/.github/workflows/pr-integrations.yml @@ -55,7 +55,9 @@ jobs: # Install Danger - name: Install Danger - run: gem install danger --no-document + run: | + gem install danger --no-document --user-install + echo "$(ruby -r rubygems -e 'puts Gem.user_dir')/bin" >> $GITHUB_PATH # Run Danger - name: Run Danger diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3b313e2 --- /dev/null +++ b/Gemfile @@ -0,0 +1,14 @@ +# STRV s.r.o. 2019 +# STRV + +source 'https://rubygems.org' + +# Ruby 3.4+ compatibility +gem 'abbrev' +gem 'nkf' + +gem 'danger', '9.5.3' +gem 'danger-duplicate_localizable_strings', git: 'https://github.com/strvcom/ios_danger-duplicate_localizable_strings', ref: 'master' +gem 'danger-swiftlint', '0.37.2' +gem 'danger-swiftformat', git: 'https://github.com/strvcom/ios-danger-ruby-swiftformat.git', ref: 'master' + From d2f45d462f8c60788fad48d749ec49ce5def00f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 15:11:44 +0100 Subject: [PATCH 47/52] feat: bundler back, danger fix --- .bundle/config | 2 + .bundler-version | 1 + .github/workflows/pr-integrations.yml | 29 +++--- Gemfile.lock | 135 ++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 .bundle/config create mode 100644 .bundler-version create mode 100644 Gemfile.lock diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 0000000..2369228 --- /dev/null +++ b/.bundle/config @@ -0,0 +1,2 @@ +--- +BUNDLE_PATH: "vendor/bundle" diff --git a/.bundler-version b/.bundler-version new file mode 100644 index 0000000..ec1cf33 --- /dev/null +++ b/.bundler-version @@ -0,0 +1 @@ +2.6.3 diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml index 3f2cfd3..3b08129 100644 --- a/.github/workflows/pr-integrations.yml +++ b/.github/workflows/pr-integrations.yml @@ -41,27 +41,24 @@ jobs: if: github.event.pull_request.draft == false timeout-minutes: 20 steps: - # Checkout the Project Repository - - name: Checkout - uses: actions/checkout@v4 - - # Istall Mise (Dev env setup tool) - - name: Install Mise - uses: jdx/mise-action@v2 + # 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: - install: true - cache: false # change to true when running on macos-latest instead of self-hosted - experimental: true + access_token: ${{ github.token }} + + # checkout the project repository + - name: Checkout + uses: actions/checkout@v1 - # Install Danger - - name: Install Danger + # install dependencies from Gemfile & CocoaPods + - name: Dependencies run: | - gem install danger --no-document --user-install - echo "$(ruby -r rubygems -e 'puts Gem.user_dir')/bin" >> $GITHUB_PATH + bundle install - # Run Danger + # run Danger - name: Run Danger - run: danger --fail-on-errors=true + run: bundle exec danger --fail-on-errors=true env: GITHUB_TOKEN: ${{ github.token }} diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..bb12309 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,135 @@ +GIT + remote: https://github.com/strvcom/ios-danger-ruby-swiftformat.git + revision: 3183ee3b72cf7884d046eb1827e3527dd9b0fbb0 + ref: master + specs: + danger-swiftformat (0.9.0) + danger-plugin-api (~> 1.0) + +GIT + remote: https://github.com/strvcom/ios_danger-duplicate_localizable_strings + revision: 4e8153f4a3e03f441230657469ca1609c48fdb75 + ref: master + specs: + danger-duplicate_localizable_strings (0.4.1) + danger (~> 9.0) + +GEM + remote: https://rubygems.org/ + specs: + abbrev (0.1.2) + activesupport (8.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) + bigdecimal (4.0.1) + claide (1.1.0) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + colored2 (3.1.2) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + cork (0.3.0) + colored2 (~> 3.1) + danger (9.5.3) + base64 (~> 0.2) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (>= 3.1, < 5) + cork (~> 0.1) + faraday (>= 0.9.0, < 3.0) + faraday-http-cache (~> 2.0) + git (>= 1.13, < 3.0) + kramdown (>= 2.5.1, < 3.0) + kramdown-parser-gfm (~> 1.0) + octokit (>= 4.0) + pstore (~> 0.1) + terminal-table (>= 1, < 5) + danger-plugin-api (1.0.0) + danger (> 2.0) + danger-swiftlint (0.37.2) + danger + rake (> 10) + thor (~> 1.4) + drb (2.2.3) + faraday (2.14.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-http-cache (2.6.1) + faraday (>= 0.8) + faraday-net_http (3.4.2) + net-http (~> 0.5) + git (2.3.3) + activesupport (>= 5.0) + addressable (~> 2.8) + process_executer (~> 1.1) + rchardet (~> 1.8) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + json (2.18.1) + kramdown (2.5.2) + rexml (>= 3.4.4) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + logger (1.7.0) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + nap (1.1.0) + net-http (0.9.1) + uri (>= 0.11.1) + nkf (0.2.0) + octokit (10.0.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) + open4 (1.3.4) + prism (1.9.0) + process_executer (1.3.0) + pstore (0.2.1) + public_suffix (7.0.5) + rake (13.3.1) + rchardet (1.10.0) + rexml (3.4.4) + sawyer (0.9.3) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + securerandom (0.4.1) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.5.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + abbrev + danger (= 9.5.3) + danger-duplicate_localizable_strings! + danger-swiftformat! + danger-swiftlint (= 0.37.2) + nkf + +BUNDLED WITH + 2.6.3 From d9802ff8454d304d95aa9565984b8dd1e2a42da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 15:15:29 +0100 Subject: [PATCH 48/52] fix: bundler --- Gemfile | 7 +-- Gemfile.lock | 127 +++++++++++++++++---------------------------------- 2 files changed, 43 insertions(+), 91 deletions(-) diff --git a/Gemfile b/Gemfile index 3b313e2..5591215 100644 --- a/Gemfile +++ b/Gemfile @@ -6,9 +6,6 @@ source 'https://rubygems.org' # Ruby 3.4+ compatibility gem 'abbrev' gem 'nkf' - -gem 'danger', '9.5.3' -gem 'danger-duplicate_localizable_strings', git: 'https://github.com/strvcom/ios_danger-duplicate_localizable_strings', ref: 'master' -gem 'danger-swiftlint', '0.37.2' -gem 'danger-swiftformat', git: 'https://github.com/strvcom/ios-danger-ruby-swiftformat.git', ref: 'master' +gem 'bundler', '~> 2.6.3' +gem 'danger', '~> 8.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index bb12309..c4b551e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,123 +1,80 @@ -GIT - remote: https://github.com/strvcom/ios-danger-ruby-swiftformat.git - revision: 3183ee3b72cf7884d046eb1827e3527dd9b0fbb0 - ref: master - specs: - danger-swiftformat (0.9.0) - danger-plugin-api (~> 1.0) - -GIT - remote: https://github.com/strvcom/ios_danger-duplicate_localizable_strings - revision: 4e8153f4a3e03f441230657469ca1609c48fdb75 - ref: master - specs: - danger-duplicate_localizable_strings (0.4.1) - danger (~> 9.0) - GEM remote: https://rubygems.org/ specs: abbrev (0.1.2) - activesupport (8.1.2) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - json - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) - base64 (0.3.0) - bigdecimal (4.0.1) claide (1.1.0) claide-plugins (0.9.2) cork nap open4 (~> 1.3) colored2 (3.1.2) - concurrent-ruby (1.3.6) - connection_pool (3.0.2) cork (0.3.0) colored2 (~> 3.1) - danger (9.5.3) - base64 (~> 0.2) + danger (8.2.3) claide (~> 1.0) claide-plugins (>= 0.9.2) - colored2 (>= 3.1, < 5) + colored2 (~> 3.1) cork (~> 0.1) - faraday (>= 0.9.0, < 3.0) + faraday (>= 0.9.0, < 2.0) faraday-http-cache (~> 2.0) - git (>= 1.13, < 3.0) - kramdown (>= 2.5.1, < 3.0) + git (~> 1.7) + kramdown (~> 2.3) kramdown-parser-gfm (~> 1.0) - octokit (>= 4.0) - pstore (~> 0.1) - terminal-table (>= 1, < 5) - danger-plugin-api (1.0.0) - danger (> 2.0) - danger-swiftlint (0.37.2) - danger - rake (> 10) - thor (~> 1.4) - drb (2.2.3) - faraday (2.14.1) - faraday-net_http (>= 2.0, < 3.5) - json - logger + no_proxy_fix + octokit (~> 4.7) + terminal-table (>= 1, < 4) + faraday (1.10.5) + 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-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) faraday-http-cache (2.6.1) faraday (>= 0.8) - faraday-net_http (3.4.2) - net-http (~> 0.5) - git (2.3.3) - activesupport (>= 5.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.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) + git (1.19.1) addressable (~> 2.8) - process_executer (~> 1.1) rchardet (~> 1.8) - i18n (1.14.8) - concurrent-ruby (~> 1.0) - json (2.18.1) kramdown (2.5.2) rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - logger (1.7.0) - minitest (6.0.2) - drb (~> 2.0) - prism (~> 1.5) + multipart-post (2.4.1) nap (1.1.0) - net-http (0.9.1) - uri (>= 0.11.1) nkf (0.2.0) - octokit (10.0.0) + no_proxy_fix (0.1.2) + octokit (4.25.1) faraday (>= 1, < 3) sawyer (~> 0.9) open4 (1.3.4) - prism (1.9.0) - process_executer (1.3.0) - pstore (0.2.1) public_suffix (7.0.5) - rake (13.3.1) rchardet (1.10.0) rexml (3.4.4) + ruby2_keywords (0.0.5) sawyer (0.9.3) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - securerandom (0.4.1) - terminal-table (4.0.0) - unicode-display_width (>= 1.1.1, < 4) - thor (1.5.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.2.0) - uri (1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) PLATFORMS arm64-darwin-24 @@ -125,10 +82,8 @@ PLATFORMS DEPENDENCIES abbrev - danger (= 9.5.3) - danger-duplicate_localizable_strings! - danger-swiftformat! - danger-swiftlint (= 0.37.2) + bundler (~> 2.6.3) + danger (~> 8.2.0) nkf BUNDLED WITH From ec76aa3fcd05bf819641d9f506c9446fe0a500d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 15:21:41 +0100 Subject: [PATCH 49/52] fix: integrations --- .github/workflows/integrations.yml | 50 +++++++++++++++++ .github/workflows/pr-integrations.yml | 78 --------------------------- 2 files changed, 50 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/integrations.yml delete mode 100644 .github/workflows/pr-integrations.yml diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml new file mode 100644 index 0000000..2627cc6 --- /dev/null +++ b/.github/workflows/integrations.yml @@ -0,0 +1,50 @@ +### Integrations workflow ### +# +# Runs Danger checks, Swiftlint and tests. +# +# github.token - is automatically assigned to your workflow by Github +# +# Note: To enable Fastlane tests, make sure that `CommonFastfile` is imported in Fastfile from the shared ios-fastlane repository. +# + +name: Integrations + +# - by default this workflow is triggered on every change in a pull request - this can have cost implications when running on macos-latest +# - read about other possible triggers on https://help.github.com/en/actions/reference/events-that-trigger-workflows +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +jobs: + integrations: + # runs-on: macos-latest # use this for repos outside of the STRV Github org + runs-on: [self-hosted, macOS, ios-integrations] + timeout-minutes: 10 + steps: + + # 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 + - name: Checkout + uses: actions/checkout@v1 + + # install dependencies from Gemfile & CocoaPods + - name: Dependencies + run: | + bundle install + + # run Danger + - name: Run Danger + run: bundle exec 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" diff --git a/.github/workflows/pr-integrations.yml b/.github/workflows/pr-integrations.yml deleted file mode 100644 index 3b08129..0000000 --- a/.github/workflows/pr-integrations.yml +++ /dev/null @@ -1,78 +0,0 @@ -### PR Integrations Workflow ### -# -# This workflow runs on pull requests and performs: -# - Danger checks -# - Unit Tests -# -# Authentication: -# - `github.token` is automatically provided by GitHub. -# -# Notes: -# - To enable Fastlane tests, ensure `CommonFastfile` is imported in your Fastfile -# from the shared ios-fastlane repository. -# - By default, this workflow runs on every PR change. Be mindful of costs when -# using macos-latest. It can also be triggered manually in GitHub. -# - See more trigger options: https://help.github.com/en/actions/reference/events-that-trigger-workflows -# -# File: .github/workflows/pr-integrations.yml - -name: Integrations (PR) - -on: - workflow_dispatch: - pull_request: - types: [opened, edited, reopened, synchronize, ready_for_review] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Common environment variables available to all jobs -env: - WORKSPACE: STRVTemplate.xcworkspace - TEST_SCHEME: STRVTemplate - DEVICES: "iPhone 13 Pro" - -jobs: - # Quality Checks (Danger) Job - quality-checks: - name: Quality Checks - runs-on: [self-hosted, macOS, mobile-ci] - if: github.event.pull_request.draft == false - timeout-minutes: 20 - steps: - # 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 - - name: Checkout - uses: actions/checkout@v1 - - # install dependencies from Gemfile & CocoaPods - - name: Dependencies - run: | - bundle install - - # run Danger - - name: Run Danger - run: bundle exec danger --fail-on-errors=true - env: - GITHUB_TOKEN: ${{ github.token }} - - # Unit Tests Job - unit-tests: - name: Unit Tests - runs-on: [self-hosted, macOS, mobile-ci] - if: github.event.pull_request.draft == false - timeout-minutes: 20 - steps: - # Checkout the Project Repository - - name: Checkout - uses: actions/checkout@v4 - - # Run Unit Tests - - name: Run Unit Tests - run: swift test --enable-code-coverage From f45fb5d2ff9c004274efd54a369c4bd80ddbcfa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 15:29:44 +0100 Subject: [PATCH 50/52] fix: ci --- .github/workflows/integrations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 2627cc6..ac3e3c4 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -18,7 +18,7 @@ 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: From c430e60c81dd5b47cb8fb7a23cdf171f822e7a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 15:42:47 +0100 Subject: [PATCH 51/52] feat: playing with mise --- .github/workflows/integrations.yml | 27 +++++++++++++++------------ .mise.toml | 2 ++ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 .mise.toml diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index ac3e3c4..2782a7a 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 @@ -22,20 +20,27 @@ jobs: 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 + # Install Ruby via Mise + - name: Install Mise + uses: jdx/mise-action@v2 + with: + install: true + cache: false + experimental: true + + # install dependencies from Gemfile - name: Dependencies - run: | - bundle install + run: bundle install # run Danger - name: Run Danger @@ -45,6 +50,4 @@ jobs: # 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/.mise.toml b/.mise.toml new file mode 100644 index 0000000..0c4809e --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +ruby = "3.3" From 00772eef279b6233f487ac67e36c17b38c5f69a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cech?= Date: Thu, 5 Mar 2026 15:54:22 +0100 Subject: [PATCH 52/52] feat: mise migration --- .github/workflows/integrations.yml | 8 +-- .mise.toml | 1 + Gemfile | 11 ---- Gemfile.lock | 90 ------------------------------ 4 files changed, 3 insertions(+), 107 deletions(-) delete mode 100644 Gemfile delete mode 100644 Gemfile.lock diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 2782a7a..4151650 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -30,7 +30,7 @@ jobs: - name: Checkout uses: actions/checkout@v1 - # Install Ruby via Mise + # Install Ruby and Danger via Mise - name: Install Mise uses: jdx/mise-action@v2 with: @@ -38,13 +38,9 @@ jobs: cache: false experimental: true - # install dependencies from Gemfile - - name: Dependencies - run: bundle install - # run Danger - name: Run Danger - run: bundle exec danger --fail-on-errors=true + run: danger --fail-on-errors=true env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.mise.toml b/.mise.toml index 0c4809e..211bb0e 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,2 +1,3 @@ [tools] ruby = "3.3" +"gem:danger" = "8.2" diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 5591215..0000000 --- a/Gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# STRV s.r.o. 2019 -# STRV - -source 'https://rubygems.org' - -# Ruby 3.4+ compatibility -gem 'abbrev' -gem 'nkf' -gem 'bundler', '~> 2.6.3' -gem 'danger', '~> 8.2.0' - diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index c4b551e..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,90 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - abbrev (0.1.2) - addressable (2.8.9) - public_suffix (>= 2.0.2, < 8.0) - claide (1.1.0) - claide-plugins (0.9.2) - cork - nap - open4 (~> 1.3) - colored2 (3.1.2) - 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) - faraday (1.10.5) - 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-em_http (1.0.0) - faraday-em_synchrony (1.0.1) - faraday-excon (1.1.0) - faraday-http-cache (2.6.1) - faraday (>= 0.8) - faraday-httpclient (1.0.1) - faraday-multipart (1.2.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) - git (1.19.1) - addressable (~> 2.8) - rchardet (~> 1.8) - kramdown (2.5.2) - rexml (>= 3.4.4) - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - multipart-post (2.4.1) - nap (1.1.0) - nkf (0.2.0) - no_proxy_fix (0.1.2) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - open4 (1.3.4) - public_suffix (7.0.5) - rchardet (1.10.0) - rexml (3.4.4) - ruby2_keywords (0.0.5) - sawyer (0.9.3) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.6.0) - -PLATFORMS - arm64-darwin-24 - ruby - -DEPENDENCIES - abbrev - bundler (~> 2.6.3) - danger (~> 8.2.0) - nkf - -BUNDLED WITH - 2.6.3