Skip to content

Commit ed038f0

Browse files
authored
Merge pull request #4 from rickhohler/fix/recursive-lock-singleton
Fix: Use NSRecursiveLock for ThreadSafeSingleton
2 parents fd6d039 + 6f345c6 commit ed038f0

8 files changed

Lines changed: 68 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
### Security
2626
- N/A
2727

28+
## [1.1.1] - 2025-12-12
29+
30+
### Added
31+
- `Data` extensions for convenient SHA256 hashing (`sha256`, `sha256Hex`)
32+
33+
### Changed
34+
- Improved error handling in `HashComputation` to use `LocalizedError` with descriptive messages
35+
36+
### Fixed
37+
- Fixed compiler warnings in `MergerTests` regarding `Sendable` conformance and unnecessary `await`
38+
- Fixed type mismatch errors in `HashComputationTests`
39+
40+
2841
## [1.1.0] - 2025-12-04
2942

3043
### Added
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
// Extensions for Data to provide convenient hashing properties
3+
public extension Data {
4+
/// SHA256 hash of the data
5+
var sha256: Data {
6+
return SHA256Strategy().compute(data: self)
7+
}
8+
9+
/// SHA256 hash of the data as a hex string
10+
var sha256Hex: String {
11+
return sha256.map { String(format: "%02x", $0) }.joined()
12+
}
13+
}

Sources/DesignAlgorithmsKit/Algorithms/Hashing/HashComputation.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ public struct HashComputation {
7171
}
7272

7373
/// Generic error for hashing failures
74-
public enum HashError: Error {
74+
public enum HashError: LocalizedError {
7575
case algorithmNotImplemented(String)
76+
case computationFailed(String)
77+
78+
public var errorDescription: String? {
79+
switch self {
80+
case .algorithmNotImplemented(let alg):
81+
return "Hash algorithm '\(alg)' is not supported on this platform"
82+
case .computationFailed(let reason):
83+
return "Hash computation failed: \(reason)"
84+
}
85+
}
7686
}

Sources/DesignAlgorithmsKit/Creational/Singleton.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ public enum SingletonError: Error {
3434
///
3535
/// ```swift
3636
/// class MySingleton: ThreadSafeSingleton {
37-
/// private init() {
37+
/// override class func createShared() -> MySingleton {
38+
/// return MySingleton()
39+
/// }
40+
///
41+
/// private override init() {
3842
/// super.init()
3943
/// // Initialize singleton
4044
/// }
@@ -50,7 +54,7 @@ public enum SingletonError: Error {
5054
open class ThreadSafeSingleton {
5155
#if !os(WASI) && !arch(wasm32)
5256
/// Lock for thread-safe initialization
53-
private static let lock = NSLock()
57+
private static let lock = NSRecursiveLock()
5458
#endif
5559

5660
/// Type-specific instance storage keyed by type identifier

Tests/DesignAlgorithmsKitTests/Behavioral/MergerTests.swift

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class MergerTests: XCTestCase {
2424
}
2525
}
2626

27-
class TestMerger: DefaultMerger<TestItem> {
27+
class TestMerger: DefaultMerger<TestItem>, @unchecked Sendable {
2828
var storage: [UUID: TestItem] = [:]
2929

3030
override func findExisting(by id: UUID) async -> TestItem? {
@@ -253,7 +253,7 @@ final class MergerTests: XCTestCase {
253253

254254
func testDefaultMergerUpsertDirectly() async throws {
255255
// Given - Create a merger that uses DefaultMerger's upsert implementation
256-
class DirectMerger: DefaultMerger<TestItem> {
256+
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
257257
var storage: [UUID: TestItem] = [:]
258258

259259
override func findExisting(by id: UUID) async -> TestItem? {
@@ -279,7 +279,7 @@ final class MergerTests: XCTestCase {
279279

280280
func testDefaultMergerUpsertWithExisting() async throws {
281281
// Given
282-
class DirectMerger: DefaultMerger<TestItem> {
282+
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
283283
var storage: [UUID: TestItem] = [:]
284284

285285
override func findExisting(by id: UUID) async -> TestItem? {
@@ -302,7 +302,7 @@ final class MergerTests: XCTestCase {
302302
let existing = TestItem(id: UUID(), name: "Existing", value: 10)
303303
let new = TestItem(id: existing.id, name: "Updated", value: 20)
304304

305-
await merger.storage[existing.id] = existing
305+
merger.storage[existing.id] = existing
306306

307307
// When - upsert with existing item
308308
let upserted = try await merger.upsert(new, strategy: .preferNew)
@@ -331,7 +331,7 @@ final class MergerTests: XCTestCase {
331331

332332
func testDefaultMergerUpsertWithCombineStrategy() async throws {
333333
// Given
334-
class DirectMerger: DefaultMerger<TestItem> {
334+
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
335335
var storage: [UUID: TestItem] = [:]
336336

337337
override func findExisting(by id: UUID) async -> TestItem? {
@@ -347,7 +347,7 @@ final class MergerTests: XCTestCase {
347347
let existing = TestItem(id: UUID(), name: "Existing", value: 10)
348348
let new = TestItem(id: existing.id, name: "New", value: 20)
349349

350-
await merger.storage[existing.id] = existing
350+
merger.storage[existing.id] = existing
351351

352352
// When - upsert with combine strategy
353353
let upserted = try await merger.upsert(new, strategy: .combine)
@@ -360,7 +360,7 @@ final class MergerTests: XCTestCase {
360360

361361
func testDefaultMergerUpsertNewItemWithCombineStrategy() async throws {
362362
// Given
363-
class DirectMerger: DefaultMerger<TestItem> {
363+
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
364364
var storage: [UUID: TestItem] = [:]
365365

366366
override func findExisting(by id: UUID) async -> TestItem? {
@@ -385,7 +385,7 @@ final class MergerTests: XCTestCase {
385385

386386
func testDefaultMergerUpsertNewItemWithPreferExistingStrategy() async throws {
387387
// Given
388-
class DirectMerger: DefaultMerger<TestItem> {
388+
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
389389
var storage: [UUID: TestItem] = [:]
390390

391391
override func findExisting(by id: UUID) async -> TestItem? {
@@ -410,7 +410,7 @@ final class MergerTests: XCTestCase {
410410

411411
func testDefaultMergerUpsertNewItemWithCustomStrategy() async throws {
412412
// Given
413-
class DirectMerger: DefaultMerger<TestItem> {
413+
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
414414
var storage: [UUID: TestItem] = [:]
415415

416416
override func findExisting(by id: UUID) async -> TestItem? {
@@ -446,7 +446,7 @@ final class MergerTests: XCTestCase {
446446

447447
func testDefaultMergerUpsertExistingWithCombineStrategy() async throws {
448448
// Given
449-
class DirectMerger: DefaultMerger<TestItem> {
449+
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
450450
var storage: [UUID: TestItem] = [:]
451451

452452
override func findExisting(by id: UUID) async -> TestItem? {
@@ -462,7 +462,7 @@ final class MergerTests: XCTestCase {
462462
let existing = TestItem(id: UUID(), name: "Existing", value: 10)
463463
let new = TestItem(id: existing.id, name: "New", value: 20)
464464

465-
await merger.storage[existing.id] = existing
465+
merger.storage[existing.id] = existing
466466

467467
// When - upsert existing with combine strategy
468468
let upserted = try await merger.upsert(new, strategy: .combine)
@@ -636,7 +636,7 @@ final class MergerTests: XCTestCase {
636636

637637
func testDefaultMergerUpsertPathThroughSuper() async throws {
638638
// Given - Test that calling super.upsert works correctly
639-
class SuperMerger: DefaultMerger<TestItem> {
639+
class SuperMerger: DefaultMerger<TestItem>, @unchecked Sendable {
640640
var storage: [UUID: TestItem] = [:]
641641
var callCount = 0
642642

@@ -664,7 +664,7 @@ final class MergerTests: XCTestCase {
664664

665665
func testDefaultMergerUpsertPathWithExistingThroughSuper() async throws {
666666
// Given
667-
class SuperMerger: DefaultMerger<TestItem> {
667+
class SuperMerger: DefaultMerger<TestItem>, @unchecked Sendable {
668668
var storage: [UUID: TestItem] = [:]
669669

670670
override func findExisting(by id: UUID) async -> TestItem? {
@@ -681,7 +681,7 @@ final class MergerTests: XCTestCase {
681681
let existing = TestItem(id: UUID(), name: "Existing", value: 10)
682682
let new = TestItem(id: existing.id, name: "New", value: 20)
683683

684-
await merger.storage[existing.id] = existing
684+
merger.storage[existing.id] = existing
685685

686686
// When - Upsert with existing item
687687
let upserted = try await merger.upsert(new, strategy: .preferNew)
@@ -706,7 +706,7 @@ final class MergerTests: XCTestCase {
706706
XCTAssertNotNil(merger)
707707

708708
// Verify we can create a subclass that properly overrides it
709-
class ProperMerger: DefaultMerger<TestItem> {
709+
class ProperMerger: DefaultMerger<TestItem>, @unchecked Sendable {
710710
override func findExisting(by id: UUID) async -> TestItem? {
711711
return nil
712712
}

Tests/DesignAlgorithmsKitTests/HashComputationTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ final class HashComputationTests: XCTestCase {
123123
let data = "Test".data(using: .utf8)!
124124

125125
XCTAssertThrowsError(try HashComputation.computeHashHex(data: data, algorithm: "invalid")) { error in
126-
guard case HashComputationError.algorithmNotSupported(let alg) = error else {
127-
XCTFail("Expected algorithmNotSupported error")
126+
guard case HashError.algorithmNotImplemented(let alg) = error else {
127+
XCTFail("Expected algorithmNotImplemented error")
128128
return
129129
}
130130
XCTAssertEqual(alg, "invalid")
@@ -199,14 +199,14 @@ final class HashComputationTests: XCTestCase {
199199

200200
// MARK: - Error Tests
201201

202-
func testHashComputationError_AlgorithmNotSupported() {
203-
let error = HashComputationError.algorithmNotSupported("test-algorithm")
202+
func testHashError_AlgorithmNotSupported() {
203+
let error = HashError.algorithmNotImplemented("test-algorithm")
204204

205205
XCTAssertEqual(error.errorDescription, "Hash algorithm 'test-algorithm' is not supported on this platform")
206206
}
207207

208-
func testHashComputationError_ComputationFailed() {
209-
let error = HashComputationError.computationFailed("test error message")
208+
func testHashError_ComputationFailed() {
209+
let error = HashError.computationFailed("test error message")
210210

211211
XCTAssertEqual(error.errorDescription, "Hash computation failed: test error message")
212212
}

docs/EXAMPLES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ let data = "Hello, World!".data(using: .utf8)!
410410
let hash = SHA256.hash(data: data)
411411
print("SHA-256 hash: \(hash.map { String(format: "%02x", $0) }.joined())")
412412

413+
// Using Data extension (New in 1.1.1)
414+
let hexHash = data.sha256Hex
415+
print("SHA-256 hex: \(hexHash)")
416+
413417
// Hash string directly
414418
let stringHash = SHA256.hash(string: "Hello, World!")
415419
print("String hash: \(stringHash.map { String(format: "%02x", $0) }.joined())")

test_import.swift

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)