From 6b9515bd19dfd940494ab9be981a403ed12723f7 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 18:05:37 -0600 Subject: [PATCH 1/2] Fix: Use NSRecursiveLock for ThreadSafeSingleton to prevent deadlocks in nested initialization --- Sources/DesignAlgorithmsKit/Creational/Singleton.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DesignAlgorithmsKit/Creational/Singleton.swift b/Sources/DesignAlgorithmsKit/Creational/Singleton.swift index 7445413..d563980 100644 --- a/Sources/DesignAlgorithmsKit/Creational/Singleton.swift +++ b/Sources/DesignAlgorithmsKit/Creational/Singleton.swift @@ -50,7 +50,7 @@ public enum SingletonError: Error { open class ThreadSafeSingleton { #if !os(WASI) && !arch(wasm32) /// Lock for thread-safe initialization - private static let lock = NSLock() + private static let lock = NSRecursiveLock() #endif /// Type-specific instance storage keyed by type identifier From 6f345c668f68427a402cd24fde49d7205c05c00e Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 18:17:44 -0600 Subject: [PATCH 2/2] Fix compiler warnings, improve tests, and update docs for v1.1.1 --- CHANGELOG.md | 13 ++++++++ .../Algorithms/Hashing/Data+Hashing.swift | 13 ++++++++ .../Algorithms/Hashing/HashComputation.swift | 12 +++++++- .../Creational/Singleton.swift | 6 +++- .../Behavioral/MergerTests.swift | 30 +++++++++---------- .../HashComputationTests.swift | 12 ++++---- docs/EXAMPLES.md | 4 +++ test_import.swift | 4 --- 8 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 Sources/DesignAlgorithmsKit/Algorithms/Hashing/Data+Hashing.swift delete mode 100644 test_import.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b7d15..4a6b31a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - N/A +## [1.1.1] - 2025-12-12 + +### Added +- `Data` extensions for convenient SHA256 hashing (`sha256`, `sha256Hex`) + +### Changed +- Improved error handling in `HashComputation` to use `LocalizedError` with descriptive messages + +### Fixed +- Fixed compiler warnings in `MergerTests` regarding `Sendable` conformance and unnecessary `await` +- Fixed type mismatch errors in `HashComputationTests` + + ## [1.1.0] - 2025-12-04 ### Added diff --git a/Sources/DesignAlgorithmsKit/Algorithms/Hashing/Data+Hashing.swift b/Sources/DesignAlgorithmsKit/Algorithms/Hashing/Data+Hashing.swift new file mode 100644 index 0000000..cb5b8a3 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Algorithms/Hashing/Data+Hashing.swift @@ -0,0 +1,13 @@ + +// Extensions for Data to provide convenient hashing properties +public extension Data { + /// SHA256 hash of the data + var sha256: Data { + return SHA256Strategy().compute(data: self) + } + + /// SHA256 hash of the data as a hex string + var sha256Hex: String { + return sha256.map { String(format: "%02x", $0) }.joined() + } +} diff --git a/Sources/DesignAlgorithmsKit/Algorithms/Hashing/HashComputation.swift b/Sources/DesignAlgorithmsKit/Algorithms/Hashing/HashComputation.swift index 08c3821..91e20f2 100644 --- a/Sources/DesignAlgorithmsKit/Algorithms/Hashing/HashComputation.swift +++ b/Sources/DesignAlgorithmsKit/Algorithms/Hashing/HashComputation.swift @@ -71,6 +71,16 @@ public struct HashComputation { } /// Generic error for hashing failures -public enum HashError: Error { +public enum HashError: LocalizedError { case algorithmNotImplemented(String) + case computationFailed(String) + + public var errorDescription: String? { + switch self { + case .algorithmNotImplemented(let alg): + return "Hash algorithm '\(alg)' is not supported on this platform" + case .computationFailed(let reason): + return "Hash computation failed: \(reason)" + } + } } diff --git a/Sources/DesignAlgorithmsKit/Creational/Singleton.swift b/Sources/DesignAlgorithmsKit/Creational/Singleton.swift index d563980..030ba40 100644 --- a/Sources/DesignAlgorithmsKit/Creational/Singleton.swift +++ b/Sources/DesignAlgorithmsKit/Creational/Singleton.swift @@ -34,7 +34,11 @@ public enum SingletonError: Error { /// /// ```swift /// class MySingleton: ThreadSafeSingleton { -/// private init() { +/// override class func createShared() -> MySingleton { +/// return MySingleton() +/// } +/// +/// private override init() { /// super.init() /// // Initialize singleton /// } diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/MergerTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/MergerTests.swift index a390229..63755ae 100644 --- a/Tests/DesignAlgorithmsKitTests/Behavioral/MergerTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/MergerTests.swift @@ -24,7 +24,7 @@ final class MergerTests: XCTestCase { } } - class TestMerger: DefaultMerger { + class TestMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -253,7 +253,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertDirectly() async throws { // Given - Create a merger that uses DefaultMerger's upsert implementation - class DirectMerger: DefaultMerger { + class DirectMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -279,7 +279,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertWithExisting() async throws { // Given - class DirectMerger: DefaultMerger { + class DirectMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -302,7 +302,7 @@ final class MergerTests: XCTestCase { let existing = TestItem(id: UUID(), name: "Existing", value: 10) let new = TestItem(id: existing.id, name: "Updated", value: 20) - await merger.storage[existing.id] = existing + merger.storage[existing.id] = existing // When - upsert with existing item let upserted = try await merger.upsert(new, strategy: .preferNew) @@ -331,7 +331,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertWithCombineStrategy() async throws { // Given - class DirectMerger: DefaultMerger { + class DirectMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -347,7 +347,7 @@ final class MergerTests: XCTestCase { let existing = TestItem(id: UUID(), name: "Existing", value: 10) let new = TestItem(id: existing.id, name: "New", value: 20) - await merger.storage[existing.id] = existing + merger.storage[existing.id] = existing // When - upsert with combine strategy let upserted = try await merger.upsert(new, strategy: .combine) @@ -360,7 +360,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertNewItemWithCombineStrategy() async throws { // Given - class DirectMerger: DefaultMerger { + class DirectMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -385,7 +385,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertNewItemWithPreferExistingStrategy() async throws { // Given - class DirectMerger: DefaultMerger { + class DirectMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -410,7 +410,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertNewItemWithCustomStrategy() async throws { // Given - class DirectMerger: DefaultMerger { + class DirectMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -446,7 +446,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertExistingWithCombineStrategy() async throws { // Given - class DirectMerger: DefaultMerger { + class DirectMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -462,7 +462,7 @@ final class MergerTests: XCTestCase { let existing = TestItem(id: UUID(), name: "Existing", value: 10) let new = TestItem(id: existing.id, name: "New", value: 20) - await merger.storage[existing.id] = existing + merger.storage[existing.id] = existing // When - upsert existing with combine strategy let upserted = try await merger.upsert(new, strategy: .combine) @@ -636,7 +636,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertPathThroughSuper() async throws { // Given - Test that calling super.upsert works correctly - class SuperMerger: DefaultMerger { + class SuperMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] var callCount = 0 @@ -664,7 +664,7 @@ final class MergerTests: XCTestCase { func testDefaultMergerUpsertPathWithExistingThroughSuper() async throws { // Given - class SuperMerger: DefaultMerger { + class SuperMerger: DefaultMerger, @unchecked Sendable { var storage: [UUID: TestItem] = [:] override func findExisting(by id: UUID) async -> TestItem? { @@ -681,7 +681,7 @@ final class MergerTests: XCTestCase { let existing = TestItem(id: UUID(), name: "Existing", value: 10) let new = TestItem(id: existing.id, name: "New", value: 20) - await merger.storage[existing.id] = existing + merger.storage[existing.id] = existing // When - Upsert with existing item let upserted = try await merger.upsert(new, strategy: .preferNew) @@ -706,7 +706,7 @@ final class MergerTests: XCTestCase { XCTAssertNotNil(merger) // Verify we can create a subclass that properly overrides it - class ProperMerger: DefaultMerger { + class ProperMerger: DefaultMerger, @unchecked Sendable { override func findExisting(by id: UUID) async -> TestItem? { return nil } diff --git a/Tests/DesignAlgorithmsKitTests/HashComputationTests.swift b/Tests/DesignAlgorithmsKitTests/HashComputationTests.swift index e6aa472..4cd2535 100644 --- a/Tests/DesignAlgorithmsKitTests/HashComputationTests.swift +++ b/Tests/DesignAlgorithmsKitTests/HashComputationTests.swift @@ -123,8 +123,8 @@ final class HashComputationTests: XCTestCase { let data = "Test".data(using: .utf8)! XCTAssertThrowsError(try HashComputation.computeHashHex(data: data, algorithm: "invalid")) { error in - guard case HashComputationError.algorithmNotSupported(let alg) = error else { - XCTFail("Expected algorithmNotSupported error") + guard case HashError.algorithmNotImplemented(let alg) = error else { + XCTFail("Expected algorithmNotImplemented error") return } XCTAssertEqual(alg, "invalid") @@ -199,14 +199,14 @@ final class HashComputationTests: XCTestCase { // MARK: - Error Tests - func testHashComputationError_AlgorithmNotSupported() { - let error = HashComputationError.algorithmNotSupported("test-algorithm") + func testHashError_AlgorithmNotSupported() { + let error = HashError.algorithmNotImplemented("test-algorithm") XCTAssertEqual(error.errorDescription, "Hash algorithm 'test-algorithm' is not supported on this platform") } - func testHashComputationError_ComputationFailed() { - let error = HashComputationError.computationFailed("test error message") + func testHashError_ComputationFailed() { + let error = HashError.computationFailed("test error message") XCTAssertEqual(error.errorDescription, "Hash computation failed: test error message") } diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 1e1fb22..1cea42f 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -410,6 +410,10 @@ let data = "Hello, World!".data(using: .utf8)! let hash = SHA256.hash(data: data) print("SHA-256 hash: \(hash.map { String(format: "%02x", $0) }.joined())") +// Using Data extension (New in 1.1.1) +let hexHash = data.sha256Hex +print("SHA-256 hex: \(hexHash)") + // Hash string directly let stringHash = SHA256.hash(string: "Hello, World!") print("String hash: \(stringHash.map { String(format: "%02x", $0) }.joined())") diff --git a/test_import.swift b/test_import.swift deleted file mode 100644 index 083b4b8..0000000 --- a/test_import.swift +++ /dev/null @@ -1,4 +0,0 @@ -import DesignAlgorithmsKit - -print("Testing import...") -print(type(of: HashComputation.self))