Skip to content
Closed
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
25 changes: 25 additions & 0 deletions Sources/Nodes/Core/NodeError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// All Contributions by Match Group
//
// Copyright © 2025 Tinder (Match Group, LLC)
//
// Licensed under the Match Group Modified 3-Clause BSD License.
// See https://github.com/Tinder/Nodes/blob/main/LICENSE for license information.
//

import Foundation

/**
* Errors that can occur within the Nodes framework.
*/
public enum NodeError: Error, LocalizedError, Equatable {
/// Error thrown when a worker of a specific type cannot be found in the context.
case workerNotFound(String)

Check failure on line 18 in Sources/Nodes/Core/NodeError.swift

View workflow job for this annotation

GitHub Actions / Lint

Lines should not have trailing whitespace (trailing_whitespace)
public var errorDescription: String? {
switch self {
case .workerNotFound(let type):
return "Worker of type '\(type)' not found in context"
}
}
}

Check failure on line 25 in Sources/Nodes/Core/NodeError.swift

View workflow job for this annotation

GitHub Actions / Lint

Files should have a single trailing newline (trailing_newline)

Check failure on line 25 in Sources/Nodes/Core/NodeError.swift

View workflow job for this annotation

GitHub Actions / Lint

Lines should not have trailing whitespace (trailing_whitespace)
20 changes: 20 additions & 0 deletions Sources/Nodes/Core/Protocols/WorkerControllerDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// All Contributions by Match Group
//
// Copyright © 2025 Tinder (Match Group, LLC)
//
// Licensed under the Match Group Modified 3-Clause BSD License.
// See https://github.com/Tinder/Nodes/blob/main/LICENSE for license information.
//

/**
* The delegate protocol for ``WorkerController`` to handle worker-related events.
*/
@preconcurrency
@MainActor
public protocol WorkerControllerDelegate: AnyObject {
/// Called when a worker of a specific type is not found in the controller.
///
/// - Parameter type: The type of worker that was not found.
func workerController(_ controller: WorkerController, didFailToFindWorkerOfType type: String)
}

Check failure on line 20 in Sources/Nodes/Core/Protocols/WorkerControllerDelegate.swift

View workflow job for this annotation

GitHub Actions / Lint

Files should have a single trailing newline (trailing_newline)

Check failure on line 20 in Sources/Nodes/Core/Protocols/WorkerControllerDelegate.swift

View workflow job for this annotation

GitHub Actions / Lint

Lines should not have trailing whitespace (trailing_whitespace)
19 changes: 15 additions & 4 deletions Sources/Nodes/Internal/WorkerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ public final class WorkerController {
/// The array of `Worker` instances managed by the ``WorkerController``.
public private(set) var workers: [Worker] = []

/// The delegate to handle worker-related events.
private weak var delegate: WorkerControllerDelegate?

/// Initializes a new ``WorkerController`` instance to manage a collection of `Worker` instances.
///
/// - Parameter workers: The array of `Worker` instances to be managed by the ``WorkerController``.
public init(workers: [Worker]) {
/// - Parameters:
/// - workers: The array of `Worker` instances to be managed by the ``WorkerController``.
/// - delegate: The delegate to handle worker-related events.
public init(workers: [Worker], delegate: WorkerControllerDelegate) {
self.workers = workers
self.delegate = delegate
}

/// Starts all `Worker` instances in the `workers` array.
Expand Down Expand Up @@ -59,9 +65,14 @@ public final class WorkerController {
/// | worker | The `Worker` instance of type `T`. |
///
/// The closure returns `Void` and throws.
public func withFirstWorker<T>(ofType type: T.Type, perform: (_ worker: T) throws -> Void) rethrows {
///
/// - Throws: ``NodeError/workerNotFound`` if no worker of the specified type exists.
public func withFirstWorker<T>(ofType type: T.Type, perform: (_ worker: T) throws -> Void) throws {
guard let worker: T = firstWorker(ofType: type)
else { return }
else {
delegate?.workerController(self, didFailToFindWorkerOfType: String(describing: type))
throw NodeError.workerNotFound(String(describing: type))
}
try perform(worker)
}

Expand Down
49 changes: 45 additions & 4 deletions Tests/NodesTests/InternalTests/WorkerControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
import Nodes
import XCTest

final class WorkerControllerTests: XCTestCase, TestCaseHelpers {

Check failure on line 14 in Tests/NodesTests/InternalTests/WorkerControllerTests.swift

View workflow job for this annotation

GitHub Actions / Lint

A 'main_type' should not be placed amongst the file type(s) 'supporting_type' (file_types_order)

private var mockWorkers: [WorkerMock]!
private var mockDelegate: WorkerControllerDelegateMock!

@MainActor
override func setUp() {
super.setUp()
tearDown(keyPath: \.mockWorkers, initialValue: [WorkerMock(), WorkerMock(), WorkerMock()])
tearDown(keyPath: \.mockDelegate, initialValue: WorkerControllerDelegateMock())
}

@MainActor
Expand Down Expand Up @@ -58,7 +60,7 @@
func testWithFirstWorkerOfType() {
let workerController: WorkerController = givenWorkerController(with: mockWorkers)
var worker: WorkerMock?
workerController.withFirstWorker(ofType: WorkerMock.self) { worker = $0 }
try? workerController.withFirstWorker(ofType: WorkerMock.self) { worker = $0 }
expect(worker) === mockWorkers.first
}

Expand All @@ -72,16 +74,55 @@
func testWithWorkersOfType() {
let workerController: WorkerController = givenWorkerController(with: mockWorkers)
var workers: [WorkerMock] = []
workerController.withWorkers(ofType: WorkerMock.self) { workers.append($0) }
try? workerController.withWorkers(ofType: WorkerMock.self) { workers.append($0) }
expect(workers) == mockWorkers
}

@MainActor
private func givenWorkerController(with workers: [Worker], start startWorkers: Bool = false) -> WorkerController {
let workerController: WorkerController = .init(workers: workers)
func testWithFirstWorkerOfTypeWhenWorkerNotFoundNotifiesDelegateAndThrows() {
let workerController: WorkerController = givenWorkerController(with: mockWorkers)
let nonExistentType = NonExistentWorkerMock.self

Check failure on line 84 in Tests/NodesTests/InternalTests/WorkerControllerTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Properties should have a type interface (explicit_type_interface)

expect { try workerController.withFirstWorker(ofType: nonExistentType) { _ in } }
.to(throwError(NodeError.workerNotFound(String(describing: nonExistentType))))

expect(mockDelegate.lastFailedWorkerType) == String(describing: nonExistentType)
}

@MainActor
func testWithFirstWorkerOfTypeWhenDelegateIsNilThrowsWithoutNotification() {
let workerController: WorkerController = givenWorkerController(with: mockWorkers, delegate: nil)
let nonExistentType = NonExistentWorkerMock.self

Check failure on line 95 in Tests/NodesTests/InternalTests/WorkerControllerTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Properties should have a type interface (explicit_type_interface)

expect { try workerController.withFirstWorker(ofType: nonExistentType) { _ in } }
.to(throwError(NodeError.workerNotFound(String(describing: nonExistentType))))

expect(mockDelegate.lastFailedWorkerType).to(beNil())

Check failure on line 100 in Tests/NodesTests/InternalTests/WorkerControllerTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Prefer Nimble operator overloads over free matcher functions (nimble_operator)
}

@MainActor
private func givenWorkerController(with workers: [Worker], start startWorkers: Bool = false, delegate: WorkerControllerDelegate? = nil) -> WorkerController {

Check failure on line 104 in Tests/NodesTests/InternalTests/WorkerControllerTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Line should be 120 characters or less; currently it has 161 characters (line_length)
let workerController: WorkerController = .init(workers: workers, delegate: delegate ?? mockDelegate)
expect(workerController).to(notBeNilAndToDeallocateAfterTest())
if startWorkers { workerController.startWorkers() }
addTeardownBlock(with: workerController) { $0.stopWorkers() }
return workerController
}
}

// MARK: - Test Doubles

private final class WorkerControllerDelegateMock: WorkerControllerDelegate {
private(set) var lastFailedWorkerType: String?

func workerController(_ controller: WorkerController, didFailToFindWorkerOfType type: String) {
lastFailedWorkerType = type
}
}

private final class NonExistentWorkerMock: Worker {
var isWorking: Bool = false

func start() {}
func stop() {}
}
Loading