From 377dedbc19cc37518ef3e2c6ac2ae0688d1f2baa Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 19:00:41 -0600 Subject: [PATCH 1/6] Feat: Add missing design patterns (Pipeline, Composite, CoR, Command, Iterator, Decorator) --- .../Behavioral/ChainOfResponsibility.swift | 82 +++++++++++++++ .../Behavioral/Command.swift | 78 +++++++++++++++ .../Behavioral/Iterator.swift | 74 ++++++++++++++ .../Behavioral/Pipeline.swift | 99 +++++++++++++++++++ .../Structural/Composite.swift | 90 +++++++++++++++++ .../Structural/Decorator.swift | 43 ++++++++ 6 files changed, 466 insertions(+) create mode 100644 Sources/DesignAlgorithmsKit/Behavioral/ChainOfResponsibility.swift create mode 100644 Sources/DesignAlgorithmsKit/Behavioral/Command.swift create mode 100644 Sources/DesignAlgorithmsKit/Behavioral/Iterator.swift create mode 100644 Sources/DesignAlgorithmsKit/Behavioral/Pipeline.swift create mode 100644 Sources/DesignAlgorithmsKit/Structural/Composite.swift create mode 100644 Sources/DesignAlgorithmsKit/Structural/Decorator.swift diff --git a/Sources/DesignAlgorithmsKit/Behavioral/ChainOfResponsibility.swift b/Sources/DesignAlgorithmsKit/Behavioral/ChainOfResponsibility.swift new file mode 100644 index 0000000..a5ee5b5 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Behavioral/ChainOfResponsibility.swift @@ -0,0 +1,82 @@ +// +// ChainOfResponsibility.swift +// DesignAlgorithmsKit +// +// Chain of Responsibility Pattern - Pass requests along a chain of handlers +// + +import Foundation + +/// Protocol for a handler in the chain +public protocol Handler: AnyObject { + /// The next handler in the chain + var nextHandler: Handler? { get set } + + /// Handle a request + /// - Parameter request: The request to handle + /// - Returns: Result if handled, nil otherwise + func handle(_ request: Any) -> Any? +} + +/// Base implementation of a handler +open class BaseHandler: Handler { + public var nextHandler: Handler? + + public init(next: Handler? = nil) { + self.nextHandler = next + } + + /// Set the next handler in the chain + /// - Parameter handler: The next handler + /// - Returns: The handler that was set (for chaining) + @discardableResult + public func setNext(_ handler: Handler) -> Handler { + self.nextHandler = handler + return handler + } + + open func handle(_ request: Any) -> Any? { + if let next = nextHandler { + return next.handle(request) + } + return nil + } +} + +/// A type-safe version of the Chain of Responsibility +public protocol TypedHandler: AnyObject { + associatedtype Request + associatedtype Response + + var nextHandler: (any TypedHandler)? { get set } + + func handle(_ request: Request) -> Response? +} + +/// Base implementation for typed handlers +open class BaseTypedHandler: TypedHandler { + public typealias Request = T + public typealias Response = R + + // We use a type-erased wrapper or force cast internally because generic protocols as types are tricky + // For simplicity in this generic pattern library, we'll store specific typed handler + public var nextTypedHandler: BaseTypedHandler? + + // Conformance to protocol (computed property due to associatedtype limits) + public var nextHandler: (any TypedHandler)? { + get { return nextTypedHandler } + set { nextTypedHandler = newValue as? BaseTypedHandler } + } + + public init() {} + + @discardableResult + public func setNext(_ handler: BaseTypedHandler) -> BaseTypedHandler { + self.nextTypedHandler = handler + return handler + } + + open func handle(_ request: T) -> R? { + return nextTypedHandler?.handle(request) + } +} diff --git a/Sources/DesignAlgorithmsKit/Behavioral/Command.swift b/Sources/DesignAlgorithmsKit/Behavioral/Command.swift new file mode 100644 index 0000000..c045211 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Behavioral/Command.swift @@ -0,0 +1,78 @@ +// +// Command.swift +// DesignAlgorithmsKit +// +// Command Pattern - Encapsulate a request as an object +// + +import Foundation + +/// Protocol for commands +public protocol Command { + /// Execute the command + func execute() + + /// Undo the command (optional) + func undo() +} + +/// Base command implementation +open class BaseCommand: Command { + public init() {} + + open func execute() { + // To be implemented by subclasses + } + + open func undo() { + // To be implemented by subclasses + } +} + +/// A command that wraps a simple closure +public class ClosureCommand: Command { + private let action: () -> Void + private let undoAction: (() -> Void)? + + public init(action: @escaping () -> Void, undoAction: (() -> Void)? = nil) { + self.action = action + self.undoAction = undoAction + } + + public func execute() { + action() + } + + public func undo() { + undoAction?() + } +} + +/// Invoker responsible for executing commands +open class CommandInvoker { + private var history: [Command] = [] + private var undoStack: [Command] = [] + + public init() {} + + /// Execute a command + public func execute(_ command: Command) { + command.execute() + history.append(command) + undoStack.removeAll() // Clear redo stack on new operation + } + + /// Undo the last command + public func undo() { + guard let command = history.popLast() else { return } + command.undo() + undoStack.append(command) + } + + /// Redo the last undone command + public func redo() { + guard let command = undoStack.popLast() else { return } + command.execute() + history.append(command) + } +} diff --git a/Sources/DesignAlgorithmsKit/Behavioral/Iterator.swift b/Sources/DesignAlgorithmsKit/Behavioral/Iterator.swift new file mode 100644 index 0000000..e697008 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Behavioral/Iterator.swift @@ -0,0 +1,74 @@ +// +// Iterator.swift +// DesignAlgorithmsKit +// +// Iterator Pattern - Access elements of a collection consistently +// + +import Foundation + +/// Protocol for iterators +public protocol Iterator { + associatedtype Element + + /// Check if there are more elements + func hasNext() -> Bool + + /// Get the next element + func next() -> Element? +} + +/// Protocol for iterable aggregates +public protocol Iterable { + associatedtype IteratorType: Iterator + + /// Create an iterator + func makeIterator() -> IteratorType +} + +/// A concrete iterator for array-based collections +public class ArrayIterator: Iterator { + private let items: [T] + private var currentIndex = 0 + + public init(_ items: [T]) { + self.items = items + } + + public func hasNext() -> Bool { + return currentIndex < items.count + } + + public func next() -> T? { + guard hasNext() else { return nil } + let item = items[currentIndex] + currentIndex += 1 + return item + } +} + +/// A concrete iterator for tree structures (depth-first) +public class TreeIterator: Iterator { + private var stack: [T] = [] + private let getChildren: (T) -> [T] + + public init(root: T, getChildren: @escaping (T) -> [T]) { + self.stack = [root] + self.getChildren = getChildren + } + + public func hasNext() -> Bool { + return !stack.isEmpty + } + + public func next() -> T? { + guard !stack.isEmpty else { return nil } + let current = stack.removeLast() + // Add children to stack in reverse order to process them in original order + let children = getChildren(current) + for child in children.reversed() { + stack.append(child) + } + return current + } +} diff --git a/Sources/DesignAlgorithmsKit/Behavioral/Pipeline.swift b/Sources/DesignAlgorithmsKit/Behavioral/Pipeline.swift new file mode 100644 index 0000000..b7c1836 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Behavioral/Pipeline.swift @@ -0,0 +1,99 @@ +// +// Pipeline.swift +// DesignAlgorithmsKit +// +// Pipeline Pattern - Process data through a sequence of stages +// + +import Foundation + +/// Protocol for a pipeline stage +public protocol DataPipelineStage { + /// The input type for this stage + associatedtype Input + + /// The output type for this stage + associatedtype Output + + /// Process the input and produce output + /// - Parameter input: Input data + /// - Returns: Processed output + /// - Throws: Error if processing fails + func process(_ input: Input) throws -> Output +} + +/// Protocol for an asynchronous pipeline stage +public protocol AsyncDataPipelineStage { + /// The input type for this stage + associatedtype Input + + /// The output type for this stage + associatedtype Output + + /// Process the input asynchronously + /// - Parameter input: Input data + /// - Returns: Processed output + /// - Throws: Error if processing fails + func process(_ input: Input) async throws -> Output +} + +/// A pipeline that executes stages sequentially +/// +/// The pipeline pattern allows processing data through a sequence of stages, +/// where the output of one stage becomes the input of the next. +open class DataPipeline { + private let operation: (Input) throws -> Output + + /// Initialize with a processing function + public init(_ operation: @escaping (Input) throws -> Output) { + self.operation = operation + } + + /// Execute the pipeline + public func execute(_ input: Input) throws -> Output { + return try operation(input) + } + + /// Append a new stage to the pipeline + public func appending(_ stage: S) -> DataPipeline where S.Input == Output { + return DataPipeline { input in + let intermediate = try self.execute(input) + return try stage.process(intermediate) + } + } + + /// Append a closure stage + public func appending(_ closure: @escaping (Output) throws -> NewOutput) -> DataPipeline { + return DataPipeline { input in + let intermediate = try self.execute(input) + return try closure(intermediate) + } + } +} + +/// An asynchronous pipeline +open class AsyncDataPipeline { + private let operation: (Input) async throws -> Output + + public init(_ operation: @escaping (Input) async throws -> Output) { + self.operation = operation + } + + public func execute(_ input: Input) async throws -> Output { + return try await operation(input) + } + + public func appending(_ stage: S) -> AsyncDataPipeline where S.Input == Output { + return AsyncDataPipeline { input in + let intermediate = try await self.execute(input) + return try await stage.process(intermediate) + } + } + + public func appending(_ closure: @escaping (Output) async throws -> NewOutput) -> AsyncDataPipeline { + return AsyncDataPipeline { input in + let intermediate = try await self.execute(input) + return try await closure(intermediate) + } + } +} diff --git a/Sources/DesignAlgorithmsKit/Structural/Composite.swift b/Sources/DesignAlgorithmsKit/Structural/Composite.swift new file mode 100644 index 0000000..23575e3 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Structural/Composite.swift @@ -0,0 +1,90 @@ +// +// Composite.swift +// DesignAlgorithmsKit +// +// Composite Pattern - Compose objects into tree structures to represent part-whole hierarchies +// + +import Foundation + +/// Protocol for components in the composite structure +public protocol Component: AnyObject { + /// The parent of this component + var parent: Component? { get set } + + /// Execute an operation on the component + func operation() + + /// Add a child component (optional operation) + func add(_ component: Component) + + /// Remove a child component (optional operation) + func remove(_ component: Component) + + /// Get a child component by index (optional operation) + func getChild(at index: Int) -> Component? +} + +/// Base implementation of a component providing default behavior +open class BaseComponent: Component { + public weak var parent: Component? + + public init() {} + + open func operation() { + // Default implementation does nothing + } + + open func add(_ component: Component) { + // Default: leaf nodes can't add children + } + + open func remove(_ component: Component) { + // Default: leaf nodes can't remove children + } + + open func getChild(at index: Int) -> Component? { + return nil + } +} + +/// A leaf component in the tree (has no children) +open class Leaf: BaseComponent { + open override func operation() { + // Perform leaf-specific operation + } +} + +/// A composite component that can contain children +open class Composite: BaseComponent { + private var children: [Component] = [] + + open override func operation() { + // Execute operation on all children + for child in children { + child.operation() + } + } + + open override func add(_ component: Component) { + children.append(component) + component.parent = self + } + + open override func remove(_ component: Component) { + children.removeAll { $0 === component } + if component.parent === self { + component.parent = nil + } + } + + open override func getChild(at index: Int) -> Component? { + guard index >= 0 && index < children.count else { return nil } + return children[index] + } + + /// Get all children + public func getChildren() -> [Component] { + return children + } +} diff --git a/Sources/DesignAlgorithmsKit/Structural/Decorator.swift b/Sources/DesignAlgorithmsKit/Structural/Decorator.swift new file mode 100644 index 0000000..c8881b6 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Structural/Decorator.swift @@ -0,0 +1,43 @@ +// +// Decorator.swift +// DesignAlgorithmsKit +// +// Decorator Pattern - Add responsibilities to objects dynamically +// + +import Foundation + +/// Protocol for decorators +/// Note: In Swift, extensions and protocol composition often replace the Decorator pattern. +/// However, the classic wrapper pattern is still useful for dynamic composition. +public protocol Decorator { + associatedtype Component + + var component: Component { get } +} + +/// Base decorator implementation +open class BaseDecorator: Decorator { + public let component: T + + public init(_ component: T) { + self.component = component + } +} + +/// Example usage wrapper for protocol objects +/// +/// ```swift +/// protocol DataService { +/// func fetchData() -> String +/// } +/// +/// class LoggingDecorator: BaseDecorator, DataService { +/// func fetchData() -> String { +/// print("Fetching data...") +/// let result = component.fetchData() +/// print("Data fetched") +/// return result +/// } +/// } +/// ``` From 0ee578ff4e9b0cb56fa87949102febc9c71c35cb Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 19:10:57 -0600 Subject: [PATCH 2/6] Feat: Add ThreadSafe and Registry DAK patterns --- .../DesignAlgorithmsKit/Core/Registry.swift | 63 +++++++++++++++ .../Structural/ThreadSafe.swift | 79 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 Sources/DesignAlgorithmsKit/Structural/ThreadSafe.swift diff --git a/Sources/DesignAlgorithmsKit/Core/Registry.swift b/Sources/DesignAlgorithmsKit/Core/Registry.swift index fd6827c..e02553a 100644 --- a/Sources/DesignAlgorithmsKit/Core/Registry.swift +++ b/Sources/DesignAlgorithmsKit/Core/Registry.swift @@ -140,3 +140,66 @@ public final class TypeRegistry: @unchecked Sendable { } } +/// A thread-safe generic registry for storing values by key. +/// +/// This class provides a base implementation for the Registry pattern using `ThreadSafe` storage. +/// It is suitable for managing collections of objects, strategies, or specifications. +/// +/// ## Usage +/// ```swift +/// class MyRegistry: Registry { ... } +/// ``` +open class Registry: @unchecked Sendable { + /// Thread-safe storage + private let storage: ThreadSafe<[Key: Value]> + + /// Initialize registry + public init() { + self.storage = ThreadSafe([:]) + } + + /// Register a value + /// - Parameters: + /// - value: Value to register + /// - key: Key to register under + open func register(_ value: Value, for key: Key) { + storage.write { $0[key] = value } + } + + /// Retrieve a value + /// - Parameter key: Key to lookup + /// - Returns: Value if found, nil otherwise + open func get(_ key: Key) -> Value? { + storage.read { $0[key] } + } + + /// Unregister a key + /// - Parameter key: Key to remove + open func unregister(_ key: Key) { + storage.write { $0.removeValue(forKey: key) } + } + + /// Get all values + /// - Returns: Array of all registered values + open func all() -> [Value] { + storage.read { Array($0.values) } + } + + /// Clear all registrations + open func removeAll() { + storage.write { $0.removeAll() } + } + + /// Lookup value (subscript support) + public subscript(key: Key) -> Value? { + get { get(key) } + set { + if let value = newValue { + register(value, for: key) + } else { + unregister(key) + } + } + } +} + diff --git a/Sources/DesignAlgorithmsKit/Structural/ThreadSafe.swift b/Sources/DesignAlgorithmsKit/Structural/ThreadSafe.swift new file mode 100644 index 0000000..576e383 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Structural/ThreadSafe.swift @@ -0,0 +1,79 @@ +// +// ThreadSafe.swift +// DesignAlgorithmsKit +// +// Thread-Safe Wrapper Pattern +// +// A generic wrapper that provides thread check/locking around a value. +// + +import Foundation + +/// A thread-safe wrapper around a value. +/// Uses a lock to ensure atomic access to the wrapped value. +/// +/// ## Usage +/// ```swift +/// // Thread-safe array +/// let safeArray = ThreadSafe<[String]>([]) +/// +/// // Thread-safe read +/// let value = safeArray.read { $0.first } +/// +/// // Thread-safe write +/// safeArray.write { $0.append("New Item") } +/// ``` +public final class ThreadSafe: @unchecked Sendable { + private var value: Value + + #if !os(WASI) && !arch(wasm32) + private let lock = NSRecursiveLock() + #endif + + /// Initialize with a value + /// - Parameter value: Initial value + public init(_ value: Value) { + self.value = value + } + + /// Read the value safely + /// - Parameter block: Closure to read the value + /// - Returns: Result of closure + public func read(_ block: (Value) throws -> Result) rethrows -> Result { + #if !os(WASI) && !arch(wasm32) + lock.lock() + defer { lock.unlock() } + #endif + return try block(value) + } + + /// Mutate the value safely + /// - Parameter block: Closure to mutate the value + /// - Returns: Result of closure + public func write(_ block: (inout Value) throws -> Result) rethrows -> Result { + #if !os(WASI) && !arch(wasm32) + lock.lock() + defer { lock.unlock() } + #endif + return try block(&value) + } + + /// Get the raw value (copy). Only works for value types. + /// Thread-safe. + public var rawValue: Value { + get { + #if !os(WASI) && !arch(wasm32) + lock.lock() + defer { lock.unlock() } + #endif + return value + } + set { + #if !os(WASI) && !arch(wasm32) + lock.lock() + defer { lock.unlock() } + #endif + value = newValue + } + } +} From bf02f2fd6887ec6c3b676c3331da73945503ba78 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 21:03:19 -0600 Subject: [PATCH 3/6] feat: Add ThreadSafeArray and ThreadSafeDictionary Implements thread-safe wrappers for standard Swift collection types using the ThreadSafe pattern. --- .../Structural/ThreadSafeArray.swift | 98 +++++++++++++++++++ .../Structural/ThreadSafeDictionary.swift | 87 ++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 Sources/DesignAlgorithmsKit/Structural/ThreadSafeArray.swift create mode 100644 Sources/DesignAlgorithmsKit/Structural/ThreadSafeDictionary.swift diff --git a/Sources/DesignAlgorithmsKit/Structural/ThreadSafeArray.swift b/Sources/DesignAlgorithmsKit/Structural/ThreadSafeArray.swift new file mode 100644 index 0000000..6ca01c2 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Structural/ThreadSafeArray.swift @@ -0,0 +1,98 @@ +// DesignAlgorithmsKit +// Structural Pattern: ThreadSafe Array +// +// A thread-safe wrapper around a standard Swift Array. +// Uses ThreadSafe<[Element]> internally. + +import Foundation + +/// A thread-safe array wrapper. +/// Provides safe concurrent access to an array using an internal lock. +public final class ThreadSafeArray: @unchecked Sendable { + private let storage: ThreadSafe<[Element]> + + public init(_ array: [Element] = []) { + self.storage = ThreadSafe(array) + } + + /// The number of elements in the array. + public var count: Int { + storage.read { $0.count } + } + + /// A Boolean value indicating whether the collection is empty. + public var isEmpty: Bool { + storage.read { $0.isEmpty } + } + + /// Adds a new element at the end of the array. + public func append(_ newElement: Element) { + storage.write { $0.append(newElement) } + } + + /// Adds the elements of a sequence to the end of the array. + public func append(contentsOf newElements: S) where S : Sequence, Element == S.Element { + storage.write { $0.append(contentsOf: newElements) } + } + + /// Removes and returns the element at the specified position. + public func remove(at index: Int) -> Element { + storage.write { $0.remove(at: index) } + } + + /// Removes all elements from the array. + public func removeAll(keepingCapacity keepCapacity: Bool = false) { + storage.write { $0.removeAll(keepingCapacity: keepCapacity) } + } + + /// Accesses the element at the specified position. + /// Note: This is not efficient for iteration. Use `read` or `map` for batch operations. + public subscript(index: Int) -> Element { + get { + storage.read { $0[index] } + } + set { + storage.write { $0[index] = newValue } + } + } + + /// Returns an array containing the results of mapping the given closure over the sequence’s elements. + public func map(_ transform: (Element) throws -> T) rethrows -> [T] { + try storage.read { try $0.map(transform) } + } + + /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. + public func compactMap(_ transform: (Element) throws -> T?) rethrows -> [T] { + try storage.read { try $0.compactMap(transform) } + } + + /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. + public func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element] { + try storage.read { try $0.filter(isIncluded) } + } + + /// Returns the first element of the sequence that satisfies the given predicate. + public func first(where predicate: (Element) throws -> Bool) rethrows -> Element? { + try storage.read { try $0.first(where: predicate) } + } + + /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. + public func contains(where predicate: (Element) throws -> Bool) rethrows -> Bool { + try storage.read { try $0.contains(where: predicate) } + } + + /// Returns a new array containing the elements of this array. + public var allElements: [Element] { + storage.read { $0 } + } + + /// Execute a block with the array for reading + public func read(_ block: ([Element]) throws -> Result) rethrows -> Result { + try storage.read(block) + } + + /// Execute a block with the array for writing + public func write(_ block: (inout [Element]) throws -> Result) rethrows -> Result { + try storage.write(block) + } +} diff --git a/Sources/DesignAlgorithmsKit/Structural/ThreadSafeDictionary.swift b/Sources/DesignAlgorithmsKit/Structural/ThreadSafeDictionary.swift new file mode 100644 index 0000000..93f3119 --- /dev/null +++ b/Sources/DesignAlgorithmsKit/Structural/ThreadSafeDictionary.swift @@ -0,0 +1,87 @@ +// DesignAlgorithmsKit +// Structural Pattern: ThreadSafe Dictionary +// +// A thread-safe wrapper around a standard Swift Dictionary. +// Uses ThreadSafe<[Key: Value]> internally. + +import Foundation + +/// A thread-safe dictionary wrapper. +/// Provides safe concurrent access to a dictionary using an internal lock. +public final class ThreadSafeDictionary: @unchecked Sendable { + private let storage: ThreadSafe<[Key: Value]> + + public init(_ dictionary: [Key: Value] = [:]) { + self.storage = ThreadSafe(dictionary) + } + + /// The number of key-value pairs in the dictionary. + public var count: Int { + storage.read { $0.count } + } + + /// A Boolean value indicating whether the dictionary is empty. + public var isEmpty: Bool { + storage.read { $0.isEmpty } + } + + /// Accesses the value associated with the given key for reading and writing. + public subscript(key: Key) -> Value? { + get { + storage.read { $0[key] } + } + set { + storage.write { $0[key] = newValue } + } + } + + /// Accesses the value with the given key. If the dictionary doesn’t contain the given key, accesses the provided default value as if the key and default value existed in the dictionary. + public subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value { + get { + storage.read { $0[key, default: defaultValue()] } + } + set { + storage.write { $0[key, default: defaultValue()] = newValue } + } + } + + /// Updates the value stored in the dictionary for the given key, or adds a new key-value pair if the key does not exist. + public func updateValue(_ value: Value, forKey key: Key) -> Value? { + storage.write { $0.updateValue(value, forKey: key) } + } + + /// Removes the value associated with the given key. + public func removeValue(forKey key: Key) -> Value? { + storage.write { $0.removeValue(forKey: key) } + } + + /// Removes all key-value pairs from the dictionary. + public func removeAll(keepingCapacity keepCapacity: Bool = false) { + storage.write { $0.removeAll(keepingCapacity: keepCapacity) } + } + + /// Returns a new dictionary containing the keys and values of this dictionary. + public var all: [Key: Value] { + storage.read { $0 } + } + + /// Returns a collection containing just the keys of the dictionary. + public var keys: [Key] { + storage.read { Array($0.keys) } + } + + /// Returns a collection containing just the values of the dictionary. + public var values: [Value] { + storage.read { Array($0.values) } + } + + /// Execute a block with the dictionary for reading + public func read(_ block: ([Key: Value]) throws -> Result) rethrows -> Result { + try storage.read(block) + } + + /// Execute a block with the dictionary for writing + public func write(_ block: (inout [Key: Value]) throws -> Result) rethrows -> Result { + try storage.write(block) + } +} From 86cf24368b7c66520ff96495fcb1e19e63ab0ebf Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 21:25:41 -0600 Subject: [PATCH 4/6] feat: Add tests for ThreadSafeArray, ThreadSafeDictionary, and Registry --- .../Core/GenericRegistryTests.swift | 68 +++++++++++ .../Structural/ThreadSafeArrayTests.swift | 110 +++++++++++++++++ .../ThreadSafeDictionaryTests.swift | 111 ++++++++++++++++++ .../Structural/ThreadSafeTests.swift | 39 ++++++ 4 files changed, 328 insertions(+) create mode 100644 Tests/DesignAlgorithmsKitTests/Core/GenericRegistryTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeTests.swift diff --git a/Tests/DesignAlgorithmsKitTests/Core/GenericRegistryTests.swift b/Tests/DesignAlgorithmsKitTests/Core/GenericRegistryTests.swift new file mode 100644 index 0000000..cd3e727 --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Core/GenericRegistryTests.swift @@ -0,0 +1,68 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class GenericRegistryTests: XCTestCase { + + func testRegistration() { + let registry = Registry() + + registry.register(1, for: "one") + XCTAssertEqual(registry.get("one"), 1) + + registry.register(2, for: "two") + XCTAssertEqual(registry.get("two"), 2) + } + + func testOverwrite() { + let registry = Registry() + + registry.register(1, for: "key") + XCTAssertEqual(registry.get("key"), 1) + + registry.register(2, for: "key") + XCTAssertEqual(registry.get("key"), 2) + } + + func testRemoval() { + let registry = Registry() + registry.register(1, for: "one") + + registry.unregister("one") + XCTAssertNil(registry.get("one")) + + registry.register(2, for: "two") + registry.removeAll() + XCTAssertNil(registry.get("two")) + } + + func testAll() { + let registry = Registry() + registry.register(1, for: "one") + registry.register(2, for: "two") + + let allValues = registry.all() + XCTAssertEqual(allValues.count, 2) + XCTAssertTrue(allValues.contains(1)) + XCTAssertTrue(allValues.contains(2)) + } + + func testConcurrency() { + let registry = Registry() + let iterations = 1000 + let expectation = self.expectation(description: "Concurrent registry access") + expectation.expectedFulfillmentCount = iterations + + DispatchQueue.concurrentPerform(iterations: iterations) { i in + registry.register(i, for: i) + expectation.fulfill() + } + + waitForExpectations(timeout: 5.0) + + // Count via iteration or if there is a count property (Registry usually doesn't expose count directly but has all().count) + XCTAssertEqual(registry.all().count, iterations) + + // Verify random element + XCTAssertEqual(registry.get(500), 500) + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift new file mode 100644 index 0000000..67bfa55 --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift @@ -0,0 +1,110 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class ThreadSafeArrayTests: XCTestCase { + + func testInitialization() { + let array = ThreadSafeArray() + XCTAssertTrue(array.isEmpty) + XCTAssertEqual(array.count, 0) + + let arrayWithItems = ThreadSafeArray([1, 2, 3]) + XCTAssertFalse(arrayWithItems.isEmpty) + XCTAssertEqual(arrayWithItems.count, 3) + } + + func testAppend() { + let array = ThreadSafeArray() + array.append(1) + XCTAssertEqual(array.count, 1) + XCTAssertEqual(array[0], 1) + + array.append(contentsOf: [2, 3]) + XCTAssertEqual(array.count, 3) + XCTAssertEqual(array.allElements, [1, 2, 3]) + } + + func testRemove() { + let array = ThreadSafeArray([1, 2, 3]) + + let removed = array.remove(at: 1) + XCTAssertEqual(removed, 2) + XCTAssertEqual(array.count, 2) + XCTAssertEqual(array.allElements, [1, 3]) + + array.removeAll() + XCTAssertTrue(array.isEmpty) + } + + func testSubscript() { + let array = ThreadSafeArray([1, 2, 3]) + XCTAssertEqual(array[0], 1) + + array[0] = 10 + XCTAssertEqual(array[0], 10) + } + + func testFunctionalMethods() { + let array = ThreadSafeArray([1, 2, 3, 4, 5]) + + // Map + let stringArray = array.map { String($0) } + XCTAssertEqual(stringArray, ["1", "2", "3", "4", "5"]) + + // Filter + let evenArray = array.filter { $0 % 2 == 0 } + XCTAssertEqual(evenArray, [2, 4]) + + // CompactMap + let optionalArray = ThreadSafeArray(["1", "2", "NaN", "4"]) + let numbers = optionalArray.compactMap { Int($0) } + XCTAssertEqual(numbers, [1, 2, 4]) + + // First(where:) + let firstEven = array.first { $0 % 2 == 0 } + XCTAssertEqual(firstEven, 2) + + // Contains(where:) + XCTAssertTrue(array.contains { $0 == 3 }) + XCTAssertFalse(array.contains { $0 == 6 }) + } + + func testConcurrency() { + let array = ThreadSafeArray() + let iterations = 1000 + let expectation = self.expectation(description: "Concurrent append") + expectation.expectedFulfillmentCount = iterations + + DispatchQueue.concurrentPerform(iterations: iterations) { i in + array.append(i) + expectation.fulfill() + } + + waitForExpectations(timeout: 5.0) + + // Should have all elements + XCTAssertEqual(array.count, iterations) + + // Count should match manual counting via read + let count = array.read { $0.count } + XCTAssertEqual(count, iterations) + } + + func testReadWriteBlock() { + let array = ThreadSafeArray([1, 2, 3]) + + // Write block + array.write { elements in + elements.append(4) + } + + XCTAssertEqual(array.count, 4) + + // Read block + let sum = array.read { elements in + return elements.reduce(0, +) + } + + XCTAssertEqual(sum, 10) + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift new file mode 100644 index 0000000..7bc564d --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift @@ -0,0 +1,111 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class ThreadSafeDictionaryTests: XCTestCase { + + func testInitialization() { + let dict = ThreadSafeDictionary() + XCTAssertTrue(dict.isEmpty) + XCTAssertEqual(dict.count, 0) + + let dictWithItems = ThreadSafeDictionary(["a": 1, "b": 2]) + XCTAssertFalse(dictWithItems.isEmpty) + XCTAssertEqual(dictWithItems.count, 2) + } + + func testSubscript() { + let dict = ThreadSafeDictionary() + + // Write + dict["a"] = 1 + XCTAssertEqual(dict["a"], 1) + XCTAssertEqual(dict.count, 1) + + // Update + dict["a"] = 2 + XCTAssertEqual(dict["a"], 2) + + // Remove via subscript + dict["a"] = nil + XCTAssertNil(dict["a"]) + XCTAssertTrue(dict.isEmpty) + } + + func testDefaultSubscript() { + let dict = ThreadSafeDictionary() + + // Read with default + XCTAssertEqual(dict["a", default: 0], 0) + + // Write with default (modify) + dict["a", default: 0] += 1 + XCTAssertEqual(dict["a"], 1) + } + + func testUpdateValue() { + let dict = ThreadSafeDictionary() + + // Insert new + let oldVal1 = dict.updateValue(1, forKey: "a") + XCTAssertNil(oldVal1) + XCTAssertEqual(dict["a"], 1) + + // Update existing + let oldVal2 = dict.updateValue(2, forKey: "a") + XCTAssertEqual(oldVal2, 1) + XCTAssertEqual(dict["a"], 2) + } + + func testRemoveValue() { + let dict = ThreadSafeDictionary(["a": 1]) + + let removed = dict.removeValue(forKey: "a") + XCTAssertEqual(removed, 1) + XCTAssertTrue(dict.isEmpty) + + let notFound = dict.removeValue(forKey: "b") + XCTAssertNil(notFound) + } + + func testProperties() { + let dict = ThreadSafeDictionary(["a": 1, "b": 2]) + + XCTAssertEqual(dict.all.count, 2) + XCTAssertEqual(dict.keys.sorted(), ["a", "b"]) + XCTAssertEqual(dict.values.sorted(), [1, 2]) + + dict.removeAll() + XCTAssertTrue(dict.isEmpty) + } + + func testConcurrency() { + let dict = ThreadSafeDictionary() + let iterations = 1000 + let expectation = self.expectation(description: "Concurrent dictionary access") + expectation.expectedFulfillmentCount = iterations + + DispatchQueue.concurrentPerform(iterations: iterations) { i in + dict[i] = i + expectation.fulfill() + } + + waitForExpectations(timeout: 5.0) + + XCTAssertEqual(dict.count, iterations) + } + + func testReadWriteBlock() { + let dict = ThreadSafeDictionary(["a": 1, "b": 2]) + + // Write block + dict.write { items in + items["c"] = 3 + } + + XCTAssertEqual(dict.count, 3) + + // Read block + let keys = dict.read { Array($0.keys).sorted() } + XCTAssertEqual(keys, ["a", "b", "c"]) + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeTests.swift new file mode 100644 index 0000000..1350ef0 --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeTests.swift @@ -0,0 +1,39 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class ThreadSafeTests: XCTestCase { + + func testInitialization() { + let safeInt = ThreadSafe(0) + XCTAssertEqual(safeInt.read { $0 }, 0) + } + + func testReadWrite() { + let safeInt = ThreadSafe(0) + + // Write + safeInt.write { $0 = 10 } + + // Read + let val = safeInt.read { $0 } + XCTAssertEqual(val, 10) + } + + func testConcurrency() { + let safeCounter = ThreadSafe(0) + let iterations = 1000 + let expectation = self.expectation(description: "Concurrent increment") + expectation.expectedFulfillmentCount = iterations + + DispatchQueue.concurrentPerform(iterations: iterations) { i in + safeCounter.write { $0 += 1 } + expectation.fulfill() + } + + waitForExpectations(timeout: 5.0) + + XCTAssertEqual(safeCounter.read { $0 }, iterations) + } + + +} From aa66de848ca6d70de3486461192595c9abbe87a5 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 21:36:28 -0600 Subject: [PATCH 5/6] test: Increase code coverage for behavioral and structural patterns --- .../ChainOfResponsibilityTests.swift | 67 ++++++++++++++ .../Behavioral/CommandTests.swift | 69 +++++++++++++++ .../Behavioral/IteratorTests.swift | 54 ++++++++++++ .../Behavioral/PipelineTests.swift | 87 +++++++++++++++++++ .../Structural/CompositeTests.swift | 76 ++++++++++++++++ .../Structural/DecoratorTests.swift | 49 +++++++++++ 6 files changed, 402 insertions(+) create mode 100644 Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift create mode 100644 Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift new file mode 100644 index 0000000..750ce9e --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift @@ -0,0 +1,67 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class ChainOfResponsibilityTests: XCTestCase { + + // MARK: - Untyped Handler Tests + + class ConcreteHandlerA: BaseHandler { + override func handle(_ request: Any) -> Any? { + if let str = request as? String, str == "A" { + return "Handled by A" + } + return super.handle(request) + } + } + + class ConcreteHandlerB: BaseHandler { + override func handle(_ request: Any) -> Any? { + if let str = request as? String, str == "B" { + return "Handled by B" + } + return super.handle(request) + } + } + + func testUntypedChain() { + let handlerA = ConcreteHandlerA() + let handlerB = ConcreteHandlerB() + + handlerA.setNext(handlerB) + + XCTAssertEqual(handlerA.handle("A") as? String, "Handled by A") + XCTAssertEqual(handlerA.handle("B") as? String, "Handled by B") + XCTAssertNil(handlerA.handle("C")) + } + + // MARK: - Typed Handler Tests + + class TypedHandlerA: BaseTypedHandler { + override func handle(_ request: String) -> String? { + if request == "A" { + return "Handled by A" + } + return super.handle(request) + } + } + + class TypedHandlerB: BaseTypedHandler { + override func handle(_ request: String) -> String? { + if request == "B" { + return "Handled by B" + } + return super.handle(request) + } + } + + func testTypedChain() { + let handlerA = TypedHandlerA() + let handlerB = TypedHandlerB() + + handlerA.setNext(handlerB) + + XCTAssertEqual(handlerA.handle("A"), "Handled by A") + XCTAssertEqual(handlerA.handle("B"), "Handled by B") + XCTAssertNil(handlerA.handle("C")) + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift new file mode 100644 index 0000000..72bc681 --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift @@ -0,0 +1,69 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class CommandTests: XCTestCase { + + func testClosureCommand() { + var value = 0 + let command = ClosureCommand( + action: { value += 1 }, + undoAction: { value -= 1 } + ) + + command.execute() + XCTAssertEqual(value, 1) + + command.undo() + XCTAssertEqual(value, 0) + } + + func testInvokerExecuteUndoRedo() { + let invoker = CommandInvoker() + var value = 0 + + let incrementCommand = ClosureCommand( + action: { value += 1 }, + undoAction: { value -= 1 } + ) + + // Execute + invoker.execute(incrementCommand) + XCTAssertEqual(value, 1) + + invoker.execute(incrementCommand) + XCTAssertEqual(value, 2) + + // Undo + invoker.undo() + XCTAssertEqual(value, 1) + + invoker.undo() + XCTAssertEqual(value, 0) + + // Redo + invoker.redo() + XCTAssertEqual(value, 1) + + invoker.redo() + XCTAssertEqual(value, 2) + } + + func testInvokerHistoryClearOnNewExecute() { + let invoker = CommandInvoker() + var value = 0 + + let cmd1 = ClosureCommand(action: { value = 1 }) + let cmd2 = ClosureCommand(action: { value = 2 }) + + invoker.execute(cmd1) + invoker.undo() + // Here undo stack has cmd1 + + // New execution should clear redo stack + invoker.execute(cmd2) + XCTAssertEqual(value, 2) + + invoker.redo() // Should do nothing because redo stack was cleared + XCTAssertEqual(value, 2) + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift new file mode 100644 index 0000000..2321d6a --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift @@ -0,0 +1,54 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class IteratorTests: XCTestCase { + + func testArrayIterator() { + let items = [1, 2, 3] + let iterator = ArrayIterator(items) + + XCTAssertTrue(iterator.hasNext()) + XCTAssertEqual(iterator.next(), 1) + + XCTAssertTrue(iterator.hasNext()) + XCTAssertEqual(iterator.next(), 2) + + XCTAssertTrue(iterator.hasNext()) + XCTAssertEqual(iterator.next(), 3) + + XCTAssertFalse(iterator.hasNext()) + XCTAssertNil(iterator.next()) + } + + func testTreeIterator() { + // Tree structure: + // 1 + // / \ + // 2 3 + // / \ + // 4 5 + + struct Node { + let value: Int + let children: [Node] + } + + let node4 = Node(value: 4, children: []) + let node5 = Node(value: 5, children: []) + let node2 = Node(value: 2, children: [node4, node5]) + let node3 = Node(value: 3, children: []) + let root = Node(value: 1, children: [node2, node3]) + + // Depth-first traversal expected: 1, 2, 4, 5, 3 + let iterator = TreeIterator(root: root) { node in + return node.children + } + + var result: [Int] = [] + while let node = iterator.next() { + result.append(node.value) + } + + XCTAssertEqual(result, [1, 2, 4, 5, 3]) + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift new file mode 100644 index 0000000..545e83d --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift @@ -0,0 +1,87 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class PipelineTests: XCTestCase { + + // MARK: - Synchronous Pipeline + + func testSyncPipelineExecution() throws { + let pipeline = DataPipeline { input in + return String(input) + } + + let result = try pipeline.execute(123) + XCTAssertEqual(result, "123") + } + + func testSyncPipelineChaining() throws { + let pipeline = DataPipeline { input in + return input * 2 + } + .appending { input in + return input + 10 + } + .appending { input in + return String(input) + } + + let result = try pipeline.execute(5) + // (5 * 2) + 10 = 20 -> "20" + XCTAssertEqual(result, "20") + } + + struct IntToStringStage: DataPipelineStage { + func process(_ input: Int) throws -> String { + return String(input) + } + } + + func testSyncPipelineWithStageObject() throws { + let pipeline = DataPipeline { $0 * 2 } + .appending(IntToStringStage()) + + let result = try pipeline.execute(5) + XCTAssertEqual(result, "10") + } + + // MARK: - Asynchronous Pipeline + + func testAsyncPipelineExecution() async throws { + let pipeline = AsyncDataPipeline { input in + try await Task.sleep(nanoseconds: 1_000_000) + return String(input) + } + + let result = try await pipeline.execute(123) + XCTAssertEqual(result, "123") + } + + func testAsyncPipelineChaining() async throws { + let pipeline = AsyncDataPipeline { input in + return input * 2 + } + .appending { input in + return input + 10 + } + .appending { input in + return String(input) + } + + let result = try await pipeline.execute(5) + XCTAssertEqual(result, "20") + } + + struct IntToStringAsyncStage: AsyncDataPipelineStage { + func process(_ input: Int) async throws -> String { + return String(input) + } + } + + func testAsyncPipelineWithStageObject() async throws { + let pipeline = AsyncDataPipeline { $0 * 2 } + .appending(IntToStringAsyncStage()) + + let result = try await pipeline.execute(5) + XCTAssertEqual(result, "10") + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift new file mode 100644 index 0000000..47d81c8 --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift @@ -0,0 +1,76 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class CompositeTests: XCTestCase { + + class TestLeaf: Leaf { + let name: String + var operationCalled = false + + init(name: String) { + self.name = name + } + + override func operation() { + operationCalled = true + } + } + + class TestComposite: Composite { + let name: String + + init(name: String) { + self.name = name + } + } + + func testTreeStructure() { + let root = TestComposite(name: "root") + let branch = TestComposite(name: "branch") + let leaf1 = TestLeaf(name: "leaf1") + let leaf2 = TestLeaf(name: "leaf2") + + root.add(branch) + root.add(leaf1) + branch.add(leaf2) + + // Check hierarchy + XCTAssertTrue(branch.parent === root) + XCTAssertTrue(leaf1.parent === root) + XCTAssertTrue(leaf2.parent === branch) + + XCTAssertEqual(root.getChildren().count, 2) + XCTAssertEqual(branch.getChildren().count, 1) + + // Check retrieval + XCTAssertTrue(root.getChild(at: 0) === branch) + XCTAssertTrue(root.getChild(at: 1) === leaf1) + } + + func testOperationPropagation() { + let root = TestComposite(name: "root") + let leaf1 = TestLeaf(name: "leaf1") + let leaf2 = TestLeaf(name: "leaf2") + + root.add(leaf1) + root.add(leaf2) + + root.operation() + + XCTAssertTrue(leaf1.operationCalled) + XCTAssertTrue(leaf2.operationCalled) + } + + func testRemove() { + let root = TestComposite(name: "root") + let leaf = TestLeaf(name: "leaf") + + root.add(leaf) + XCTAssertEqual(root.getChildren().count, 1) + XCTAssertNotNil(leaf.parent) + + root.remove(leaf) + XCTAssertEqual(root.getChildren().count, 0) + XCTAssertNil(leaf.parent) + } +} diff --git a/Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift new file mode 100644 index 0000000..8d2ff27 --- /dev/null +++ b/Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift @@ -0,0 +1,49 @@ +import XCTest +@testable import DesignAlgorithmsKit + +final class DecoratorTests: XCTestCase { + + // Component Interface + protocol TextComponent { + func render() -> String + } + + // Concrete Component + class SimpleText: TextComponent { + func render() -> String { + return "Text" + } + } + + // Decorator + class BoldDecorator: BaseDecorator, TextComponent { + func render() -> String { + return "" + component.render() + "" + } + } + + class ItalicDecorator: BaseDecorator, TextComponent { + func render() -> String { + return "" + component.render() + "" + } + } + + func testDecoratorChain() { + let simple = SimpleText() + XCTAssertEqual(simple.render(), "Text") + + let bold = BoldDecorator(simple) + XCTAssertEqual(bold.render(), "Text") + + let boldItalic = ItalicDecorator(bold) + XCTAssertEqual(boldItalic.render(), "Text") + } + + func testBaseDecoratorProperties() { + let simple = SimpleText() + let decorator = BaseDecorator(simple) + + // Check access to underlying component + XCTAssertTrue(decorator.component === simple) + } +} From 8aef84e825ae41e0b8ba27c8e46b83e011f2cfe0 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 21:42:51 -0600 Subject: [PATCH 6/6] test: Add comprehensive unit tests for all DAK patterns --- .../ChainOfResponsibilityTests.swift | 53 +++++++++++++ .../Behavioral/CommandTests.swift | 79 +++++++++++++++++-- .../Behavioral/IteratorTests.swift | 55 +++++++++++-- .../Behavioral/PipelineTests.swift | 47 ++++++++++- .../Structural/CompositeTests.swift | 56 ++++++++++++- .../Structural/DecoratorTests.swift | 37 ++++++++- .../Structural/ThreadSafeArrayTests.swift | 66 +++++++++++----- .../ThreadSafeDictionaryTests.swift | 41 +++++++++- 8 files changed, 395 insertions(+), 39 deletions(-) diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift index 750ce9e..c87e3f5 100644 --- a/Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/ChainOfResponsibilityTests.swift @@ -34,6 +34,38 @@ final class ChainOfResponsibilityTests: XCTestCase { XCTAssertNil(handlerA.handle("C")) } + func testSingleHandlerChain() { + let handler = ConcreteHandlerA() + XCTAssertEqual(handler.handle("A") as? String, "Handled by A") + XCTAssertNil(handler.handle("B")) + } + + func testLongChain() { + // Build a chain of 100 handlers + // Only the last one handles "LAST" + + let first = BaseHandler() + var current = first + + for _ in 0..<99 { + let next = BaseHandler() + current.setNext(next) + current = next + } + + class LastHandler: BaseHandler { + override func handle(_ request: Any) -> Any? { + if request as? String == "LAST" { return "DONE" } + return super.handle(request) + } + } + + current.setNext(LastHandler()) + + XCTAssertEqual(first.handle("LAST") as? String, "DONE") + XCTAssertNil(first.handle("NOWHERE")) + } + // MARK: - Typed Handler Tests class TypedHandlerA: BaseTypedHandler { @@ -64,4 +96,25 @@ final class ChainOfResponsibilityTests: XCTestCase { XCTAssertEqual(handlerA.handle("B"), "Handled by B") XCTAssertNil(handlerA.handle("C")) } + + func testDynamicReconfiguration() { + let h1 = TypedHandlerA() + let h2 = TypedHandlerB() + let h3 = TypedHandlerA() // Another A + + // Chain: A -> B + h1.setNext(h2) + XCTAssertEqual(h1.handle("B"), "Handled by B") + + // Chain: A -> A (h3) -> B + h1.setNext(h3) + h3.setNext(h2) + + // Should still work, just passes through h3 (which ignores "B") to h2 + XCTAssertEqual(h1.handle("B"), "Handled by B") + + // Remove h2: A -> A + h3.nextHandler = nil + XCTAssertNil(h1.handle("B")) + } } diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift index 72bc681..dad0bc3 100644 --- a/Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/CommandTests.swift @@ -52,18 +52,87 @@ final class CommandTests: XCTestCase { let invoker = CommandInvoker() var value = 0 - let cmd1 = ClosureCommand(action: { value = 1 }) - let cmd2 = ClosureCommand(action: { value = 2 }) + // Cmd1: Set to 1 + let cmd1 = ClosureCommand( + action: { value = 1 }, + undoAction: { value = 0 } + ) - invoker.execute(cmd1) - invoker.undo() + // Cmd2: Set to 2 + let cmd2 = ClosureCommand( + action: { value = 2 }, + undoAction: { value = 1 } + ) + + invoker.execute(cmd1) // Val: 1 + invoker.undo() // Val: 0 // Here undo stack has cmd1 // New execution should clear redo stack - invoker.execute(cmd2) + invoker.execute(cmd2) // Val: 2 XCTAssertEqual(value, 2) invoker.redo() // Should do nothing because redo stack was cleared XCTAssertEqual(value, 2) + + // Undo cmd2 + invoker.undo() + XCTAssertEqual(value, 1) // Should revert to 1 (if cmd2's undo is correct) or 0 if we assume absolute state? + // Ah, the undoAction logic above is slightly flawed if we want true history. + // But the test is verifying the INVOKER behavior (managing stacks), not the command logic itself per se. + // Let's assume the undo actions are correct for this test. + } + + func testDoubleUndoRedoSafety() { + let invoker = CommandInvoker() + var value = 0 + let cmd = ClosureCommand( + action: { value += 1 }, + undoAction: { value -= 1 } + ) + + invoker.execute(cmd) + XCTAssertEqual(value, 1) + + invoker.undo() + XCTAssertEqual(value, 0) + + // Undo again (empty stack) + invoker.undo() + XCTAssertEqual(value, 0) + + invoker.redo() + XCTAssertEqual(value, 1) + + // Redo again (empty stack) + invoker.redo() + XCTAssertEqual(value, 1) + } + + class ComplexCommand: Command { + var execCount = 0 + var undoCount = 0 + + func execute() { execCount += 1 } + func undo() { undoCount += 1 } + } + + func testCommandOrder() { + let invoker = CommandInvoker() + let cmd1 = ComplexCommand() + let cmd2 = ComplexCommand() + + invoker.execute(cmd1) + invoker.execute(cmd2) + + XCTAssertEqual(cmd1.execCount, 1) + XCTAssertEqual(cmd2.execCount, 1) + + invoker.undo() // cmd2 + XCTAssertEqual(cmd2.undoCount, 1) + XCTAssertEqual(cmd1.undoCount, 0) + + invoker.undo() // cmd1 + XCTAssertEqual(cmd1.undoCount, 1) } } diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift index 2321d6a..47e2a8f 100644 --- a/Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/IteratorTests.swift @@ -3,6 +3,8 @@ import XCTest final class IteratorTests: XCTestCase { + // MARK: - Array Iterator + func testArrayIterator() { let items = [1, 2, 3] let iterator = ArrayIterator(items) @@ -20,6 +22,30 @@ final class IteratorTests: XCTestCase { XCTAssertNil(iterator.next()) } + func testEmptyArrayIterator() { + let items: [Int] = [] + let iterator = ArrayIterator(items) + + XCTAssertFalse(iterator.hasNext()) + XCTAssertNil(iterator.next()) + } + + func testSingleElementArrayIterator() { + let items = [1] + let iterator = ArrayIterator(items) + + XCTAssertTrue(iterator.hasNext()) + XCTAssertEqual(iterator.next(), 1) + XCTAssertFalse(iterator.hasNext()) + } + + // MARK: - Tree Iterator + + struct Node { + let value: Int + let children: [Node] + } + func testTreeIterator() { // Tree structure: // 1 @@ -28,11 +54,6 @@ final class IteratorTests: XCTestCase { // / \ // 4 5 - struct Node { - let value: Int - let children: [Node] - } - let node4 = Node(value: 4, children: []) let node5 = Node(value: 5, children: []) let node2 = Node(value: 2, children: [node4, node5]) @@ -51,4 +72,28 @@ final class IteratorTests: XCTestCase { XCTAssertEqual(result, [1, 2, 4, 5, 3]) } + + func testSingleNodeTreeIterator() { + let root = Node(value: 1, children: []) + let iterator = TreeIterator(root: root) { $0.children } + + XCTAssertTrue(iterator.hasNext()) + let val = iterator.next() + XCTAssertEqual(val?.value, 1) + XCTAssertFalse(iterator.hasNext()) + } + + func testDeepTreePath() { + // 1 -> 2 -> 3 + let node3 = Node(value: 3, children: []) + let node2 = Node(value: 2, children: [node3]) + let node1 = Node(value: 1, children: [node2]) + + let iterator = TreeIterator(root: node1) { $0.children } + + XCTAssertEqual(iterator.next()?.value, 1) + XCTAssertEqual(iterator.next()?.value, 2) + XCTAssertEqual(iterator.next()?.value, 3) + XCTAssertNil(iterator.next()) + } } diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift index 545e83d..5d9ac65 100644 --- a/Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/PipelineTests.swift @@ -3,7 +3,7 @@ import XCTest final class PipelineTests: XCTestCase { - // MARK: - Synchronous Pipeline + // MARK: - Synchronous Pipeline Tests func testSyncPipelineExecution() throws { let pipeline = DataPipeline { input in @@ -44,7 +44,37 @@ final class PipelineTests: XCTestCase { XCTAssertEqual(result, "10") } - // MARK: - Asynchronous Pipeline + enum PipelineError: Error { + case stageFailure + } + + func testSyncPipelineErrorPropagation() { + let pipeline = DataPipeline { _ in + throw PipelineError.stageFailure + } + .appending { $0 + 1 } + + XCTAssertThrowsError(try pipeline.execute(1)) { error in + XCTAssertEqual(error as? PipelineError, .stageFailure) + } + } + + func testSyncPipelineMidChainFailure() { + let pipeline = DataPipeline { $0 * 2 } + .appending { val -> Int in + if val == 4 { throw PipelineError.stageFailure } + return val + } + .appending { String($0) } + + // Should succeed + XCTAssertNoThrow(try pipeline.execute(1)) // 1 -> 2 -> "2" + + // Should fail + XCTAssertThrowsError(try pipeline.execute(2)) // 2 -> 4 -> Error + } + + // MARK: - Asynchronous Pipeline Tests func testAsyncPipelineExecution() async throws { let pipeline = AsyncDataPipeline { input in @@ -84,4 +114,17 @@ final class PipelineTests: XCTestCase { let result = try await pipeline.execute(5) XCTAssertEqual(result, "10") } + + func testAsyncPipelineErrorPropagation() async { + let pipeline = AsyncDataPipeline { _ in + throw PipelineError.stageFailure + } + + do { + _ = try await pipeline.execute(1) + XCTFail("Should have thrown error") + } catch { + XCTAssertEqual(error as? PipelineError, .stageFailure) + } + } } diff --git a/Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift index 47d81c8..b615cc6 100644 --- a/Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Structural/CompositeTests.swift @@ -49,10 +49,12 @@ final class CompositeTests: XCTestCase { func testOperationPropagation() { let root = TestComposite(name: "root") + let branch = TestComposite(name: "branch") let leaf1 = TestLeaf(name: "leaf1") let leaf2 = TestLeaf(name: "leaf2") - root.add(leaf1) + root.add(branch) + branch.add(leaf1) root.add(leaf2) root.operation() @@ -73,4 +75,56 @@ final class CompositeTests: XCTestCase { XCTAssertEqual(root.getChildren().count, 0) XCTAssertNil(leaf.parent) } + + func testRemoveNonChild() { + let root = TestComposite(name: "root") + let leaf1 = TestLeaf(name: "leaf1") + let leaf2 = TestLeaf(name: "leaf2") // Not added + + root.add(leaf1) + + // Verify removing non-child does nothing bad + root.remove(leaf2) + XCTAssertEqual(root.getChildren().count, 1) + + // Leaf2 still has no parent + XCTAssertNil(leaf2.parent) + } + + func testMovingChild() { + let root1 = TestComposite(name: "root1") + let root2 = TestComposite(name: "root2") + let leaf = TestLeaf(name: "leaf") + + root1.add(leaf) + XCTAssertTrue(leaf.parent === root1) + + // Remove from 1, add to 2 + root1.remove(leaf) + XCTAssertNil(leaf.parent) + + root2.add(leaf) + XCTAssertTrue(leaf.parent === root2) + } + + func testDeepRecursion() { + // Create a deep structure + let root = TestComposite(name: "root") + var current = root + + // 50 levels deep + for i in 0..<50 { + let next = TestComposite(name: "level\(i)") + current.add(next) + current = next + } + + let leaf = TestLeaf(name: "bottom") + current.add(leaf) + + // Execute operation from top + root.operation() + + XCTAssertTrue(leaf.operationCalled) + } } diff --git a/Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift index 8d2ff27..f833cd1 100644 --- a/Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Structural/DecoratorTests.swift @@ -4,7 +4,7 @@ import XCTest final class DecoratorTests: XCTestCase { // Component Interface - protocol TextComponent { + protocol TextComponent: AnyObject { func render() -> String } @@ -28,6 +28,12 @@ final class DecoratorTests: XCTestCase { } } + class UppercaseDecorator: BaseDecorator, TextComponent { + func render() -> String { + return component.render().uppercased() + } + } + func testDecoratorChain() { let simple = SimpleText() XCTAssertEqual(simple.render(), "Text") @@ -35,10 +41,39 @@ final class DecoratorTests: XCTestCase { let bold = BoldDecorator(simple) XCTAssertEqual(bold.render(), "Text") + // Italic(Bold(Text)) let boldItalic = ItalicDecorator(bold) XCTAssertEqual(boldItalic.render(), "Text") } + func testOrderOfDecorators() { + let simple = SimpleText() + + // Upper(Bold(Text)) -> TEXT + let upperBold = UppercaseDecorator(BoldDecorator(simple)) + XCTAssertEqual(upperBold.render(), "TEXT") + + // Bold(Upper(Text)) -> TEXT + let boldUpper = BoldDecorator(UppercaseDecorator(simple)) + XCTAssertEqual(boldUpper.render(), "TEXT") + } + + func testMultipleLayers() { + // Deep stacking + var component: TextComponent = SimpleText() + + // Wrap 10 times in Bold + for _ in 0..<10 { + component = BoldDecorator(component) + } + + let result = component.render() + let openingTags = String(repeating: "", count: 10) + let closingTags = String(repeating: "", count: 10) + + XCTAssertEqual(result, "\(openingTags)Text\(closingTags)") + } + func testBaseDecoratorProperties() { let simple = SimpleText() let decorator = BaseDecorator(simple) diff --git a/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift index 67bfa55..2457e2d 100644 --- a/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeArrayTests.swift @@ -3,6 +3,8 @@ import XCTest final class ThreadSafeArrayTests: XCTestCase { + // MARK: - Basic Functionality + func testInitialization() { let array = ThreadSafeArray() XCTAssertTrue(array.isEmpty) @@ -13,7 +15,7 @@ final class ThreadSafeArrayTests: XCTestCase { XCTAssertEqual(arrayWithItems.count, 3) } - func testAppend() { + func testAppendAndHelpers() { let array = ThreadSafeArray() array.append(1) XCTAssertEqual(array.count, 1) @@ -36,7 +38,7 @@ final class ThreadSafeArrayTests: XCTestCase { XCTAssertTrue(array.isEmpty) } - func testSubscript() { + func testSubscriptModifiers() { let array = ThreadSafeArray([1, 2, 3]) XCTAssertEqual(array[0], 1) @@ -69,42 +71,64 @@ final class ThreadSafeArrayTests: XCTestCase { XCTAssertFalse(array.contains { $0 == 6 }) } - func testConcurrency() { + func testReadWriteBlock() { + let array = ThreadSafeArray([1, 2, 3]) + + // Write block + array.write { elements in + elements.append(4) + elements[0] = 99 + } + + // Read block + let sum = array.read { elements in + return elements.reduce(0, +) + } + + // 99 + 2 + 3 + 4 = 108 + XCTAssertEqual(sum, 108) + } + + // MARK: - Concurrency Stress Tests + + func testConcurrentAppendAndRead() { let array = ThreadSafeArray() - let iterations = 1000 + let iterations = 2000 // High iteration count let expectation = self.expectation(description: "Concurrent append") expectation.expectedFulfillmentCount = iterations DispatchQueue.concurrentPerform(iterations: iterations) { i in + // Mix of writes and reads array.append(i) + + // Randomly read + if i % 100 == 0 { + _ = array.count + } + expectation.fulfill() } - waitForExpectations(timeout: 5.0) + waitForExpectations(timeout: 10.0) // Should have all elements XCTAssertEqual(array.count, iterations) - - // Count should match manual counting via read - let count = array.read { $0.count } - XCTAssertEqual(count, iterations) } - func testReadWriteBlock() { - let array = ThreadSafeArray([1, 2, 3]) - - // Write block - array.write { elements in - elements.append(4) - } + func testConcurrentRacesInMap() { + let array = ThreadSafeArray() + // Fill array first + for i in 0..<100 { array.append(i) } - XCTAssertEqual(array.count, 4) + let expectation = self.expectation(description: "Concurrent map") + expectation.expectedFulfillmentCount = 10 - // Read block - let sum = array.read { elements in - return elements.reduce(0, +) + DispatchQueue.concurrentPerform(iterations: 10) { _ in + // Simultaneously map (read) + let _ = array.map { $0 * 2 } + expectation.fulfill() } - XCTAssertEqual(sum, 10) + waitForExpectations(timeout: 5.0) } } diff --git a/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift index 7bc564d..1d1a5d2 100644 --- a/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Structural/ThreadSafeDictionaryTests.swift @@ -40,6 +40,9 @@ final class ThreadSafeDictionaryTests: XCTestCase { // Write with default (modify) dict["a", default: 0] += 1 XCTAssertEqual(dict["a"], 1) + + // Verify it persists + XCTAssertEqual(dict["a"], 1) } func testUpdateValue() { @@ -71,8 +74,11 @@ final class ThreadSafeDictionaryTests: XCTestCase { let dict = ThreadSafeDictionary(["a": 1, "b": 2]) XCTAssertEqual(dict.all.count, 2) - XCTAssertEqual(dict.keys.sorted(), ["a", "b"]) - XCTAssertEqual(dict.values.sorted(), [1, 2]) + let keys = dict.keys.sorted() + let values = dict.values.sorted() + + XCTAssertEqual(keys, ["a", "b"]) + XCTAssertEqual(values, [1, 2]) dict.removeAll() XCTAssertTrue(dict.isEmpty) @@ -80,20 +86,47 @@ final class ThreadSafeDictionaryTests: XCTestCase { func testConcurrency() { let dict = ThreadSafeDictionary() - let iterations = 1000 + let iterations = 2000 let expectation = self.expectation(description: "Concurrent dictionary access") expectation.expectedFulfillmentCount = iterations DispatchQueue.concurrentPerform(iterations: iterations) { i in + // Concurrent writes dict[i] = i + + // Occasional read + if i % 100 == 0 { + _ = dict[i] + } + expectation.fulfill() } - waitForExpectations(timeout: 5.0) + waitForExpectations(timeout: 10.0) XCTAssertEqual(dict.count, iterations) } + func testConcurrentUpdatesSameKey() { + let dict = ThreadSafeDictionary(["counter": 0]) + let iterations = 1000 + let expectation = self.expectation(description: "Concurrent increment") + expectation.expectedFulfillmentCount = iterations + + // Attempt to race on same key + // NOTE: This test doesn't guarantee atomic increment because read & write are separate operations in `dict["counter"] += 1` + // We must use `write` block for atomic updates. + + DispatchQueue.concurrentPerform(iterations: iterations) { _ in + dict.write { $0["counter", default: 0] += 1 } + expectation.fulfill() + } + + waitForExpectations(timeout: 5.0) + + XCTAssertEqual(dict["counter"], iterations) + } + func testReadWriteBlock() { let dict = ThreadSafeDictionary(["a": 1, "b": 2])