Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ let package = Package(
targets: [
.target(
name: "SCInject",
dependencies: [
.target(name: "SCInjectObjc"),
]
),
.target(
name: "SCInjectObjc",
dependencies: []
),
.testTarget(
Expand Down
43 changes: 34 additions & 9 deletions Sources/SCInject/Container.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,8 @@ public final class DefaultContainer: Container {

public func resolve<T>(_ type: T.Type) -> T {
guard let instance = tryResolve(type) else {
let message = "Failed to resolve given type -- TYPE=\(type)"
Exception.raise(reason: message)
fatalError(message)
ContainerError.raise(reason: "Failed to resolve given type", type: "\(type)", name: nil)
fatalError()
}
return instance
}
Expand All @@ -85,9 +84,8 @@ public final class DefaultContainer: Container {

public func resolve<T>(_ type: T.Type, name: RegistrationName) -> T {
guard let instance = tryResolve(type, name: name) else {
let message = "Failed to resolve given type -- TYPE=\(type) NAME=\(name.rawValue)"
Exception.raise(reason: message)
fatalError(message)
ContainerError.raise(reason: "Failed to resolve given type", type: "\(type)", name: name.rawValue)
fatalError()
}
return instance
}
Expand All @@ -106,6 +104,34 @@ public final class DefaultContainer: Container {
tryResolve(type: type, name: name, container: self)
}

/// Validates the dependency graph to ensure that all dependencies
/// can be successfully resolved.
///
/// This method attempts to resolve each dependency registered in the container,
/// verifying that all dependencies can be satisfied without errors. If any
/// dependency cannot be resolved, an error is thrown to indicate
/// the issue.
///
/// The validation process is performed recursively. If the container has a
/// parent container, the method will also validate the parent container's
/// dependency graph.
///
/// - Throws:
/// - `ContainerError` if any dependency within the graph cannot be resolved.
///
/// - Note:
/// This method is useful for ensuring that the dependency injection setup
/// is correct. It can be particularly valuable during development or testing
/// to catch configuration issues early.
public func validate() throws {
try ContainerError.rethrow {
for resolver in resolvers {
_ = resolver.value.resolve(with: self)
}
}
try parent?.validate()
}

// MARK: - Private

private func register<T>(
Expand All @@ -117,9 +143,8 @@ public final class DefaultContainer: Container {
lock.lock(); defer { lock.unlock() }
let identifier = identifier(of: type, name: name)
if resolvers[identifier] != nil {
let message = "Given type is already registered -- TYPE=\(type) NAME=\(name?.rawValue ?? "nil")"
Exception.raise(reason: message)
fatalError(message)
ContainerError.raise(reason: "Given type is already registered", type: "\(type)", name: name?.rawValue)
fatalError()
}
resolvers[identifier] = makeResolver(scope ?? defaultScope, closure: closure)
}
Expand Down
62 changes: 62 additions & 0 deletions Sources/SCInject/ContainerError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// Copyright 2024 Marcin Iwanicki and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import SCInjectObjc

/// A `ContainerError` represents an error that occurs during the registration
/// or resolution in the dependency injection container.
///
/// This error type is used to encapsulate specific details about the failure,
/// including the reason for the error, the type of the service that caused the
/// error, and an optional name associated with the service.
public struct ContainerError: Error {
/// A description of the reason why the error occurred.
public let reason: String

/// The type of the dependency that caused the error.
public let type: String

/// The name associated with the dependency.
public let name: String?

static func rethrow(_ closure: () -> Void) throws {
try rethrow {
try ContainerException.catch(closure)
}
}

static func raise(reason: String, type: String, name: String?) {
ContainerException.raise(reason: reason, type: type, name: nil)
}

// MARK: - Private

private static func rethrow(_ closure: () throws -> Void) rethrows {
do {
try closure()
} catch {
let nsError = error as NSError
// swiftlint:disable:next force_cast
let reason = nsError.userInfo[NSLocalizedDescriptionKey] as! String

// swiftlint:disable:next force_cast
let type = nsError.userInfo[CSContainerExceptionTypeKey] as! String
let name = nsError.userInfo[CSContainerExceptionNameKey] as? String
throw ContainerError(reason: reason, type: type, name: name)
}
}
}
59 changes: 59 additions & 0 deletions Sources/SCInjectObjc/ContainerException.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Copyright 2024 Marcin Iwanicki and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#import "ContainerException.h"

NSString* const CSContainerExceptionTypeKey = @"SCContainerExceptionTypeKey";
NSString* const CSContainerExceptionNameKey = @"SCContainerExceptionNameKey";

@implementation ContainerException

+ (void)raiseWithReason:(NSString *)reason type:(NSString *)type name:(nullable NSString *)name {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{
CSContainerExceptionTypeKey: type
}];
if (name) {
userInfo[CSContainerExceptionNameKey] = name;
}
@throw [NSException exceptionWithName:@"SCBasicObjc.Exception"
reason:reason
userInfo:userInfo];
}

+ (void)catchException:(__attribute__((noescape)) void (^)(void))operation
withError:(NSError **)error {
@try {
operation();
}
@catch (NSException *exception) {
if (error) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{
NSLocalizedDescriptionKey: exception.reason,
NSLocalizedFailureReasonErrorKey: exception.name,
CSContainerExceptionTypeKey: exception.userInfo[CSContainerExceptionTypeKey]
}];
NSString *name = exception.userInfo[CSContainerExceptionNameKey];
if (name) {
userInfo[CSContainerExceptionNameKey] = name;
}
*error = [NSError errorWithDomain:@"SCBasicObjc.Exception"
code:1
userInfo:userInfo];
}
}
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
// limitations under the License.
//

import Foundation
#import <Foundation/Foundation.h>

final class Exception: NSException {
static func raise(reason: String, userInfo: [String: String] = [:]) {
Exception(
name: NSExceptionName(rawValue: "SCInject.Exception"),
reason: reason,
userInfo: userInfo
).raise()
}
}
NS_ASSUME_NONNULL_BEGIN

extern NSString* const CSContainerExceptionTypeKey;
extern NSString* const CSContainerExceptionNameKey;

@interface ContainerException : NSObject

+ (void)raiseWithReason:(NSString *)reason type:(NSString *)type name:(nullable NSString *)name NS_SWIFT_NAME(raise(reason:type:name:));
+ (void)catchException:(__attribute__((noescape)) void (^)(void))operation
withError:(NSError **)error __attribute__((swift_error(nonnull_error)));

@end

NS_ASSUME_NONNULL_END
38 changes: 38 additions & 0 deletions Tests/SCInject/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,44 @@ final class ContainerTests: XCTestCase {
XCTAssertTrue(class1 !== class1_name)
XCTAssertTrue(class2?.value !== class1_name)
}

func testValidate() {
// Given
let second: RegistrationName = .init(rawValue: "second")
let container = DefaultContainer()
container.register(TestClass1.self) { _ in
TestClass1(value: "TestClass1_Instance")
}
container.register(TestClass1.self, name: second) { _ in
TestClass1(value: "TestClass1_Second_Instance")
}
container.register(TestClass2.self) { r in
TestClass2(value: r.resolve(TestClass1.self, name: second))
}

// When / Then
XCTAssertNoThrow(try container.validate())
}

func testValidate_missingNamedType() {
// Given
let second: RegistrationName = .init(rawValue: "second")
let container = DefaultContainer()
container.register(TestClass1.self) { _ in
TestClass1(value: "TestClass1_Instance")
}
container.register(TestClass2.self) { r in
TestClass2(value: r.resolve(TestClass1.self, name: second))
}

// When / Then
XCTAssertThrowsError(try container.validate()) { error in
let error = error as? ContainerError
XCTAssertEqual(error?.reason, "Failed to resolve given type")
XCTAssertEqual(error?.type, "TestClass1")
XCTAssertNil(error?.name)
}
}
}

// swiftlint:enable identifier_name