diff --git a/Sources/ServiceLifecycle/PreambleService.swift b/Sources/ServiceLifecycle/PreambleService.swift new file mode 100644 index 0000000..684d53b --- /dev/null +++ b/Sources/ServiceLifecycle/PreambleService.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftServiceLifecycle open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftServiceLifecycle project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Logging + +/// Service that runs a preamble closure before running a child service +public struct PreambleService: Service { + let preamble: @Sendable () async throws -> Void + let service: S + + /// Initialize PreambleService + /// - Parameters: + /// - service: Child service + /// - preamble: Preamble closure to run before running the service + public init(service: S, preamble: @escaping @Sendable () async throws -> Void) { + self.service = service + self.preamble = preamble + } + + public func run() async throws { + try await preamble() + try await service.run() + } +} + +extension PreambleService where S == ServiceGroup { + /// Initialize PreambleService with a child ServiceGroup + /// - Parameters: + /// - services: Array of services to create ServiceGroup from + /// - logger: Logger used by ServiceGroup + /// - preamble: Preamble closure to run before starting the child services + public init(services: [Service], logger: Logger, _ preamble: @escaping @Sendable () async throws -> Void) { + self.init( + service: ServiceGroup(configuration: .init(services: services, logger: logger)), + preamble: preamble + ) + } + + /// Initialize PreambleService with a child ServiceGroup + /// - Parameters: + /// - services: Array of service configurations to create ServiceGroup from + /// - logger: Logger used by ServiceGroup + /// - preamble: Preamble closure to run before starting the child services + public init( + services: [ServiceGroupConfiguration.ServiceConfiguration], + logger: Logger, + _ preamble: @escaping @Sendable () async throws -> Void + ) { + self.init( + service: ServiceGroup(configuration: .init(services: services, logger: logger)), + preamble: preamble + ) + } +} diff --git a/Tests/ServiceLifecycleTests/ServiceGroupTests.swift b/Tests/ServiceLifecycleTests/ServiceGroupTests.swift index 0120251..4fa8a4e 100644 --- a/Tests/ServiceLifecycleTests/ServiceGroupTests.swift +++ b/Tests/ServiceLifecycleTests/ServiceGroupTests.swift @@ -1476,6 +1476,85 @@ final class ServiceGroupTests: XCTestCase { } } + func testPreambleService() async throws { + struct TestService: Service { + let continuation: AsyncStream.Continuation + + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + func run() async throws { + continuation.yield(1) + } + } + let (stream, continuation) = AsyncStream.makeStream(of: Int.self) + let preambleService = PreambleService(service: TestService(continuation: continuation)) { + continuation.yield(0) + } + var logger = Logger(label: "Tests") + logger.logLevel = .debug + + let serviceGroup = ServiceGroup( + services: [preambleService], + logger: logger + ) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + + var eventIterator = stream.makeAsyncIterator() + await XCTAsyncAssertEqual(await eventIterator.next(), 0) + await XCTAsyncAssertEqual(await eventIterator.next(), 1) + + group.cancelAll() + } + } + + func testPreambleServices() async throws { + struct TestService: Service { + let continuation: AsyncStream.Continuation + + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + func run() async throws { + continuation.yield(1) + } + } + let (stream, continuation) = AsyncStream.makeStream(of: Int.self) + var logger = Logger(label: "Tests") + logger.logLevel = .debug + let preambleService = PreambleService( + services: [ + TestService(continuation: continuation), + TestService(continuation: continuation), + ], + logger: logger + ) { continuation.yield(0) } + + let serviceGroup = ServiceGroup( + services: [preambleService], + logger: logger + ) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + + var eventIterator = stream.makeAsyncIterator() + await XCTAsyncAssertEqual(await eventIterator.next(), 0) + await XCTAsyncAssertEqual(await eventIterator.next(), 1) + await XCTAsyncAssertEqual(await eventIterator.next(), 1) + + group.cancelAll() + } + } + // MARK: - Helpers private func makeServiceGroup(