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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions Sources/DesignAlgorithmsKit/Algorithms/Hashing/Data+Hashing.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
}
8 changes: 6 additions & 2 deletions Sources/DesignAlgorithmsKit/Creational/Singleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// }
Expand All @@ -50,7 +54,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
Expand Down
30 changes: 15 additions & 15 deletions Tests/DesignAlgorithmsKitTests/Behavioral/MergerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class MergerTests: XCTestCase {
}
}

class TestMerger: DefaultMerger<TestItem> {
class TestMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

override func findExisting(by id: UUID) async -> TestItem? {
Expand Down Expand Up @@ -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<TestItem> {
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

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

func testDefaultMergerUpsertWithExisting() async throws {
// Given
class DirectMerger: DefaultMerger<TestItem> {
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

override func findExisting(by id: UUID) async -> TestItem? {
Expand All @@ -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)
Expand Down Expand Up @@ -331,7 +331,7 @@ final class MergerTests: XCTestCase {

func testDefaultMergerUpsertWithCombineStrategy() async throws {
// Given
class DirectMerger: DefaultMerger<TestItem> {
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

override func findExisting(by id: UUID) async -> TestItem? {
Expand All @@ -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)
Expand All @@ -360,7 +360,7 @@ final class MergerTests: XCTestCase {

func testDefaultMergerUpsertNewItemWithCombineStrategy() async throws {
// Given
class DirectMerger: DefaultMerger<TestItem> {
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

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

func testDefaultMergerUpsertNewItemWithPreferExistingStrategy() async throws {
// Given
class DirectMerger: DefaultMerger<TestItem> {
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

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

func testDefaultMergerUpsertNewItemWithCustomStrategy() async throws {
// Given
class DirectMerger: DefaultMerger<TestItem> {
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

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

func testDefaultMergerUpsertExistingWithCombineStrategy() async throws {
// Given
class DirectMerger: DefaultMerger<TestItem> {
class DirectMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

override func findExisting(by id: UUID) async -> TestItem? {
Expand All @@ -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)
Expand Down Expand Up @@ -636,7 +636,7 @@ final class MergerTests: XCTestCase {

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

Expand Down Expand Up @@ -664,7 +664,7 @@ final class MergerTests: XCTestCase {

func testDefaultMergerUpsertPathWithExistingThroughSuper() async throws {
// Given
class SuperMerger: DefaultMerger<TestItem> {
class SuperMerger: DefaultMerger<TestItem>, @unchecked Sendable {
var storage: [UUID: TestItem] = [:]

override func findExisting(by id: UUID) async -> TestItem? {
Expand All @@ -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)
Expand All @@ -706,7 +706,7 @@ final class MergerTests: XCTestCase {
XCTAssertNotNil(merger)

// Verify we can create a subclass that properly overrides it
class ProperMerger: DefaultMerger<TestItem> {
class ProperMerger: DefaultMerger<TestItem>, @unchecked Sendable {
override func findExisting(by id: UUID) async -> TestItem? {
return nil
}
Expand Down
12 changes: 6 additions & 6 deletions Tests/DesignAlgorithmsKitTests/HashComputationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
}
Expand Down
4 changes: 4 additions & 0 deletions docs/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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())")
Expand Down
4 changes: 0 additions & 4 deletions test_import.swift

This file was deleted.