Skip to content

Add preamble service that runs closure before running service #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
64 changes: 64 additions & 0 deletions Sources/ServiceLifecycle/PreambleService.swift
Original file line number Diff line number Diff line change
@@ -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<S: Service>: 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
)
}
}
79 changes: 79 additions & 0 deletions Tests/ServiceLifecycleTests/ServiceGroupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,85 @@ final class ServiceGroupTests: XCTestCase {
}
}

func testPreambleService() async throws {
struct TestService: Service {
let continuation: AsyncStream<Int>.Continuation

init(continuation: AsyncStream<Int>.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<Int>.Continuation

init(continuation: AsyncStream<Int>.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(
Expand Down
Loading