Skip to content

Commit

Permalink
Add library evolution support (#13)
Browse files Browse the repository at this point in the history
* Add library evolution support

* Update doc and align with AnyPublisher with Combine’s swiftinterface
  • Loading branch information
Kyle-Ye authored Dec 27, 2023
1 parent 1b8a26b commit 5113764
Show file tree
Hide file tree
Showing 18 changed files with 136 additions and 217 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/compatibility_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ jobs:
run: swift --version
- name: Run tests against Apple's Combine
run: make test-compatibility

- name: Build with library evolution
run: make library-evolution
- name: Generate swiftinterface
run: make module-interface
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ swift-version:
test-compatibility:
OPENCOMBINE_COMPATIBILITY_TEST=1 $(SWIFT_EXE) test

library-evolution:
OPENCOMBINE_LIBRARY_EVOLUTION=1 $(SWIFT_EXE) build

module-interface:
xcodebuild build -scheme OpenCombine -sdk macosx -destination "platform=macOS" BUILD_LIBRARY_FOR_DISTRIBUTION=1

gyb:
$(shell ./utils/recursively_gyb.sh)

Expand All @@ -34,5 +40,7 @@ clean:
test-release \
swift-version \
test-compatibility-debug \
library-evolution \
module-interface \
gyb \
clean
25 changes: 19 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ let openCombineTarget: Target = .target(
.target(
name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi]))
)
),
],
exclude: [
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
"Publishers/Publishers.Catch.swift.gyb",
]
)
let openCombineFoundationTarget: Target = .target(
Expand All @@ -40,7 +40,7 @@ let openCombineFoundationTarget: Target = .target(
.target(
name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi]))
)
),
]
)
let openCombineDispatchTarget: Target = .target(
Expand Down Expand Up @@ -79,7 +79,7 @@ let package = Package(
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
]
),
openCombineTarget,
Expand All @@ -92,9 +92,9 @@ let package = Package(

// MARK: Helpers

extension Array where Element == Platform {
extension [Platform] {
func except(_ exceptions: [Platform]) -> [Platform] {
return filter { !exceptions.contains($0) }
filter { !exceptions.contains($0) }
}
}

Expand All @@ -105,6 +105,19 @@ func envEnable(_ key: String) -> Bool {
return value == "1"
}

let enableLibraryEvolution = envEnable("OPENCOMBINE_LIBRARY_EVOLUTION")
if enableLibraryEvolution {
let libraryEvolutionSetting: SwiftSetting = .unsafeFlags([
"-Xfrontend", "-enable-library-evolution",
])
let targets = [openCombineTarget, openCombineFoundationTarget, openCombineDispatchTarget]
for target in targets {
var settings = target.swiftSettings ?? []
settings.append(libraryEvolutionSetting)
target.swiftSettings = settings
}
}

let enableCompatibilityTest = envEnable("OPENCOMBINE_COMPATIBILITY_TEST")
if enableCompatibilityTest {
var settings = openCombineTestsTarget.swiftSettings ?? []
Expand Down
27 changes: 20 additions & 7 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ let openCombineTarget: Target = .target(
.target(
name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi]))
)
),
],
exclude: [
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
"Publishers/Publishers.Catch.swift.gyb",
]
)
let openCombineFoundationTarget: Target = .target(
Expand All @@ -41,7 +41,7 @@ let openCombineFoundationTarget: Target = .target(
.target(
name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi]))
)
),
]
)
let openCombineDispatchTarget: Target = .target(
Expand All @@ -66,7 +66,7 @@ let openCombineTestsTarget: Target = .testTarget(
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombine",targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
Expand All @@ -80,7 +80,7 @@ let package = Package(
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
]
),
openCombineTarget,
Expand All @@ -93,9 +93,9 @@ let package = Package(

// MARK: Helpers

extension Array where Element == Platform {
extension [Platform] {
func except(_ exceptions: [Platform]) -> [Platform] {
return filter { !exceptions.contains($0) }
filter { !exceptions.contains($0) }
}
}

Expand All @@ -106,6 +106,19 @@ func envEnable(_ key: String) -> Bool {
return value == "1"
}

let enableLibraryEvolution = envEnable("OPENCOMBINE_LIBRARY_EVOLUTION")
if enableLibraryEvolution {
let libraryEvolutionSetting: SwiftSetting = .unsafeFlags([
"-Xfrontend", "-enable-library-evolution",
])
let targets = [openCombineTarget, openCombineFoundationTarget, openCombineDispatchTarget]
for target in targets {
var settings = target.swiftSettings ?? []
settings.append(libraryEvolutionSetting)
target.swiftSettings = settings
}
}

let enableCompatibilityTest = envEnable("OPENCOMBINE_COMPATIBILITY_TEST")
if enableCompatibilityTest {
var settings = openCombineTestsTarget.swiftSettings ?? []
Expand Down
102 changes: 35 additions & 67 deletions Sources/OpenCombine/AnyPublisher.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
//
// AnyPublisher.swift
//
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
// Audited for Combine 2023

extension Publisher {

/// Wraps this publisher with a type eraser.
///
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher`` to
/// the downstream subscriber, rather than this publisher’s actual type.
/// This form of _type erasure_ preserves abstraction across API boundaries, such as
/// different modules.
/// When you expose your publishers as the `AnyPublisher` type, you can change
/// the underlying implementation over time without affecting existing clients.
/// Use ``Publisher/eraseToAnyPublisher()`` to expose an instance of ``AnyPublisher`` to the downstream subscriber, rather than this publisher’s actual type.
/// This form of _type erasure_ preserves abstraction across API boundaries, such as different modules.
/// When you expose your publishers as the ``AnyPublisher`` type, you can change the underlying implementation over time without affecting existing clients.
///
/// The following example shows two types that each have a `publisher` property.
/// `TypeWithSubject` exposes this property as its actual type, `PassthroughSubject`,
/// while `TypeWithErasedSubject` uses `eraseToAnyPublisher()` to expose it as
/// an `AnyPublisher`. As seen in the output, a caller from another module can access
/// `TypeWithSubject.publisher` as its native type. This means you can’t change your
/// publisher to a different type without breaking the caller. By comparison,
/// `TypeWithErasedSubject.publisher` appears to callers as an `AnyPublisher`, so you
/// can change the underlying publisher type at will.
/// The following example shows two types that each have a `publisher` property. `TypeWithSubject` exposes this property as its actual type, ``PassthroughSubject``, while `TypeWithErasedSubject` uses ``Publisher/eraseToAnyPublisher()`` to expose it as an ``AnyPublisher``. As seen in the output, a caller from another module can access `TypeWithSubject.publisher` as its native type. This means you can’t change your publisher to a different type without breaking the caller. By comparison, `TypeWithErasedSubject.publisher` appears to callers as an ``AnyPublisher``, so you can change the underlying publisher type at will.
///
/// public class TypeWithSubject {
/// public let publisher: some Publisher = PassthroughSubject<Int,Never>()
Expand All @@ -48,100 +37,79 @@ extension Publisher {
/// - Returns: An ``AnyPublisher`` wrapping this publisher.
@inlinable
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
return .init(self)
AnyPublisher(self)
}
}

/// A type-erasing publisher.
/// A publisher that performs type erasure by wrapping another publisher.
///
/// Use `AnyPublisher` to wrap a publisher whose type has details you don’t want to expose
/// across API boundaries, such as different modules. Wrapping a `Subject` with
/// `AnyPublisher` also prevents callers from accessing its `send(_:)` method. When you
/// use type erasure this way, you can change the underlying publisher implementation over
/// time without affecting existing clients.
/// ``AnyPublisher`` is a concrete implementation of ``Publisher`` that has no significant properties of its own, and passes through elements and completion values from its upstream publisher.
///
/// You can use OpenCombine’s `eraseToAnyPublisher()` operator to wrap a publisher with
/// `AnyPublisher`.
public struct AnyPublisher<Output, Failure: Error>
: CustomStringConvertible,
CustomPlaygroundDisplayConvertible
/// Use ``AnyPublisher`` to wrap a publisher whose type has details you don’t want to expose across API boundaries, such as different modules. Wrapping a ``Subject`` with ``AnyPublisher`` also prevents callers from accessing its ``Subject/send(_:)`` method. When you use type erasure this way, you can change the underlying publisher implementation over time without affecting existing clients.
///
/// You can use OpenCombine’s ``Publisher/eraseToAnyPublisher()`` operator to wrap a publisher with ``AnyPublisher``.
@frozen
public struct AnyPublisher<Output, Failure: Error>: CustomStringConvertible, CustomPlaygroundDisplayConvertible
{
@usableFromInline
internal let box: PublisherBoxBase<Output, Failure>
let box: PublisherBoxBase<Output, Failure>

public var description: String { "AnyPublisher" }

public var playgroundDescription: Any { description }

/// Creates a type-erasing publisher to wrap the provided publisher.
///
/// - Parameter publisher: A publisher to wrap with a type-eraser.
@inlinable
public init<PublisherType: Publisher>(_ publisher: PublisherType)
where Output == PublisherType.Output, Failure == PublisherType.Failure
{
public init<P>(_ publisher: P) where Output == P.Output, Failure == P.Failure, P: Publisher {
// If this has already been boxed, avoid boxing again
if let erased = publisher as? AnyPublisher<Output, Failure> {
box = erased.box
} else {
box = PublisherBox(base: publisher)
box = PublisherBox(publisher)
}
}

public var description: String {
return "AnyPublisher"
}

public var playgroundDescription: Any {
return description
}
}

extension AnyPublisher: Publisher {

/// This function is called to attach the specified `Subscriber` to this `Publisher`
/// by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
@inlinable
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S: Subscriber {
box.receive(subscriber: subscriber)
}
}

/// A type-erasing base class. Its concrete subclass is generic over the underlying
/// publisher.
@usableFromInline
internal class PublisherBoxBase<Output, Failure: Error>: Publisher {

@_fixed_layout
class PublisherBoxBase<Output, Failure: Error>: Publisher {
@inlinable
internal init() {}
init() {}

@inlinable deinit {}

@usableFromInline
internal func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
func receive<S>(subscriber _: S) where Output == S.Input, Failure == S.Failure, S: Subscriber {
abstractMethod()
}
}

@usableFromInline
internal final class PublisherBox<PublisherType: Publisher>
: PublisherBoxBase<PublisherType.Output, PublisherType.Failure>
{
@_fixed_layout
final class PublisherBox<Base: Publisher>: PublisherBoxBase<Base.Output, Base.Failure> {
@usableFromInline
internal let base: PublisherType
let base: Base

@inlinable
internal init(base: PublisherType) {
init(_ base: Base) {
self.base = base
super.init()
}

@inlinable deinit {}

@inlinable
override internal func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
override final func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input
{
base.receive(subscriber: subscriber)
}
Expand Down
Loading

0 comments on commit 5113764

Please sign in to comment.