Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46007658a1 | |||
| c4c7f2172d | |||
| 5b0a21a0b9 | |||
| f4e191b2ff | |||
| c275e51cdc | |||
| a84105133c | |||
| ef3ebd965a | |||
| a08b99c886 | |||
| 3398499540 | |||
| 1bf193ddaa | |||
| 3a88dfd76b | |||
| bd0b69d7cb | |||
| dba76c3c41 | |||
| 5863492753 | |||
| 2f2e16ee1f | |||
| bca131c2a4 | |||
| e999fafdce | |||
| b38830e0f1 | |||
| 525405f64d | |||
| 693d1145f8 | |||
| 2f9ddc2229 | |||
| bcd1b727f8 | |||
| 5d1034fcc0 | |||
| 2378f3d97e | |||
| 4a965830e7 | |||
| 9eabadb7c9 | |||
| dcfaec2c9d | |||
| 219ee38119 | |||
| 3a5389d398 | |||
| 69ead1c8fb | |||
| 8e6404592e | |||
| 14b7ced2fe | |||
| d7b9e87f6d | |||
| 4fd04b8a00 | |||
| 5f92ee05d2 | |||
| bdd703abb3 | |||
| e41c48a5cd | |||
| df0b8b08db | |||
| 7056143b99 | |||
| 0a965ba60a | |||
| 7dfaa4edea | |||
| 3e8f2774a4 | |||
| 68e9bbe164 | |||
| 0f71c33d72 | |||
| 3f61648f82 | |||
| c621ceb267 | |||
| 2aa297ec39 | |||
| 9cb27bb91b | |||
| d74f68da86 | |||
| f68dcd520f | |||
| 432fd4f48f | |||
| 9c6bbda0c4 | |||
| 3990ec2afb | |||
| 39dd9e40bf | |||
| fd7c0459b9 | |||
| f7145e7fa5 | |||
| ecd4766129 | |||
| e00a6f06fc | |||
| 23ee3a4b7b | |||
| 9c913124eb | |||
| 7ddd15b334 | |||
| 72753ef93c | |||
| 816426b48c | |||
| 47fb390081 | |||
| 1d3327f6bf | |||
| eb7478d430 | |||
| f69621f0e2 | |||
| 7f3cccf1ae | |||
| ec037dbb3d | |||
| 8a39f35d3f | |||
| 7fb92bffc6 | |||
| e441ea3048 | |||
| 22f7b6d10d | |||
| 7431d21c9c | |||
| 1d901fca7f | |||
| 9834eab0ea | |||
| 1ce9660ce9 | |||
| 313d6befa6 | |||
| 8c7f061892 | |||
| 2ac2470579 | |||
| 57c9ae8590 | |||
| d57c878651 | |||
| 7fa91778c2 | |||
| d15e604764 | |||
| 07c7a98d72 | |||
| 01ef05be1f | |||
| beee9d0d51 | |||
| aacd1a326c | |||
| 5528adcc67 | |||
| 1b810d0536 | |||
| 8b25238154 | |||
| 9b9915bde7 | |||
| 27f01e5f21 | |||
| 739eb47409 | |||
| 14d5a90e89 | |||
| 0e869bc861 | |||
| 2f38069166 | |||
| 97d07d0a14 | |||
| d3888a3808 | |||
| d2b8709afb | |||
| a28177e9c5 | |||
| cef19fce4b | |||
| 7f6bba62de |
@@ -0,0 +1,3 @@
|
||||
*.swift.gyb linguist-language=Swift
|
||||
|
||||
**/GENERATED-* linguist-generated=true
|
||||
+111
@@ -2,6 +2,7 @@
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
/.swiftpm
|
||||
|
||||
# Created by https://www.gitignore.io/api/Xcode
|
||||
# Edit at https://www.gitignore.io/?templates=Xcode
|
||||
@@ -42,3 +43,113 @@ DerivedData/
|
||||
# End of https://www.gitignore.io/api/Xcode
|
||||
|
||||
.idea
|
||||
|
||||
# Created by https://www.gitignore.io/api/Python
|
||||
# Edit at https://www.gitignore.io/?templates=Python
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# End of https://www.gitignore.io/api/Python
|
||||
|
||||
@@ -75,3 +75,35 @@ generic_type_name:
|
||||
attributes:
|
||||
always_on_line_above:
|
||||
- "@usableFromInline"
|
||||
|
||||
custom_rules:
|
||||
no_foundation_dependency:
|
||||
included: Sources/OpenCombine/
|
||||
name: "No Foundation Dependency"
|
||||
regex: "^import.*Foundation.*$"
|
||||
message: "We don't want to depend on Foundation"
|
||||
severity: error
|
||||
|
||||
no_dispatch_dependency:
|
||||
included: Sources/OpenCombine/
|
||||
name: "No Dispatch Dependency"
|
||||
regex: "^import.*Dispatch.*$"
|
||||
message: "We don't want to depend on Dispatch"
|
||||
severity: error
|
||||
|
||||
inheritance_colon:
|
||||
name: "Inheritance Colon"
|
||||
regex: '\s[A-Z_]\w*(<[\w\s:\.,]+>)?(?: +:\s*|:(?:\s{0}|\s{2,}))([\[|\(]*\S)'
|
||||
message: "Colons should be next to the identifier of the inheriting type"
|
||||
severity: warning
|
||||
match_kinds:
|
||||
- identifier
|
||||
- typeidentifier
|
||||
|
||||
dictionary_type_colon:
|
||||
name: "Dictionary Type Colon"
|
||||
regex: '\[\w+(?:(?:\s{0}|\s{2,}):| :(?:\s{0}|\s{2,})\w)'
|
||||
message: "Colon should be surrounded by a single whitespace in a dictionary literal"
|
||||
severity: warning
|
||||
match_kinds:
|
||||
- typeidentifier
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1100"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombineTests"
|
||||
BuildableName = "OpenCombineTests"
|
||||
BlueprintName = "OpenCombineTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
enableThreadSanitizer = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombineTests"
|
||||
BuildableName = "OpenCombineTests"
|
||||
BlueprintName = "OpenCombineTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+45
-13
@@ -2,29 +2,43 @@ language: generic
|
||||
|
||||
addons:
|
||||
homebrew:
|
||||
taps:
|
||||
- danger/tap
|
||||
packages:
|
||||
- swiftlint
|
||||
- danger-swift
|
||||
update: true
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- .build
|
||||
- ~/.danger-swift
|
||||
- ~/.swiftenv
|
||||
- ~/Library/Caches/Homebrew
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Ubuntu 16.04 | Swift 5.1 | Tests"
|
||||
- name: "Ubuntu 16.04 | Swift 5.1.1 | Tests"
|
||||
os: linux
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: SWIFT_VERSION="swift-5.1-DEVELOPMENT-SNAPSHOT-2019-06-16-a" OPENCOMBINE_TEST="YES"
|
||||
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Tests"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
env: SWIFT_VERSION="5.0" CODE_COVERAGE="YES" OPENCOMBINE_TEST="YES"
|
||||
# - name: "iOS 13.0 | Swift 5.1 | Compatibility Tests"
|
||||
# os: osx
|
||||
# osx_image: xcode11
|
||||
# env: SWIFT_VERSION="5.1" OPENCOMBINE_COMPATIBILITY_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | SwiftLint"
|
||||
- name: "iOS 13.1 | Swift 5.1.1 | Compatibility Tests"
|
||||
os: osx
|
||||
osx_image: xcode11.1
|
||||
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_COMPATIBILITY_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Code Quality"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
env: SWIFT_VERSION="5.0" SWIFT_LINT="YES"
|
||||
env: SWIFT_VERSION="5.0" RUN_DANGER="YES" SWIFT_LINT="YES"
|
||||
|
||||
before_cache:
|
||||
- brew cleanup
|
||||
|
||||
before_install:
|
||||
- if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
cat /proc/cpuinfo;
|
||||
@@ -38,21 +52,39 @@ install:
|
||||
fi
|
||||
script:
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
swift test -c debug --enable-code-coverage --sanitize thread;
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug;
|
||||
else
|
||||
make test-debug;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
swift test -c release;
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug-sanitize-thread;
|
||||
else
|
||||
make test-debug-sanitize-thread;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-release;
|
||||
else
|
||||
make test-release;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_COMPATIBILITY_TEST == "YES" ]]; then
|
||||
swift package generate-xcodeproj --xcconfig-overrides iOS-Combine-Compatibility.xcconfig;
|
||||
set -o pipefail && xcodebuild -scheme OpenCombine-Package -sdk iphonesimulator13.0 -destination "platform=iOS Simulator,name=iPhone Xs,OS=13.0" build test | xcpretty;
|
||||
make generate-compatibility-xcodeproj;
|
||||
set -o pipefail && xcodebuild -scheme OpenCombine-Package -sdk iphonesimulator13.1 -destination "platform=iOS Simulator,name=iPhone 11,OS=13.1" build test | xcpretty;
|
||||
fi
|
||||
- if [[ $SWIFT_LINT == "YES" ]]; then
|
||||
swiftlint lint --strict --reporter "emoji";
|
||||
fi
|
||||
- if [[ $RUN_DANGER == "YES" ]]; then
|
||||
danger-swift ci;
|
||||
fi
|
||||
after_success:
|
||||
- if [[ $CODE_COVERAGE == "YES" ]]; then
|
||||
swift package generate-xcodeproj --enable-code-coverage;
|
||||
make generate-xcodeproj;
|
||||
xcodebuild -scheme OpenCombine-Package build test | xcpretty;
|
||||
bash <(curl -s https://codecov.io/bash);
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import Danger
|
||||
import Foundation
|
||||
|
||||
extension StringProtocol {
|
||||
func dropSuffix<S: StringProtocol>(_ suffix: S) -> SubSequence {
|
||||
if hasSuffix(suffix) {
|
||||
return self[..<index(endIndex, offsetBy: -suffix.count)]
|
||||
} else {
|
||||
return self[...]
|
||||
}
|
||||
}
|
||||
|
||||
func directoryAndFileName() -> (SubSequence, SubSequence) {
|
||||
let lastPathSeparator = lastIndex(of: "/")
|
||||
if let lastPathSeparator = lastPathSeparator {
|
||||
return (self[..<lastPathSeparator], self[index(after: lastPathSeparator)...])
|
||||
} else {
|
||||
return (".", self[...])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let danger = Danger()
|
||||
|
||||
let allCreatedAndModified = danger.git.createdFiles + danger.git.modifiedFiles
|
||||
|
||||
do {
|
||||
// Fail if the committer modified a GYB template but forgot to run `make gyb`.
|
||||
|
||||
let modifiedTemplates = allCreatedAndModified.filter { $0.hasSuffix(".gyb") }
|
||||
|
||||
for modifiedTemplate in modifiedTemplates {
|
||||
let (directory, filename) = modifiedTemplate.directoryAndFileName()
|
||||
let generated = "\(directory)/GENERATED-\(filename.dropSuffix(".gyb"))"
|
||||
|
||||
if !allCreatedAndModified.contains(generated) {
|
||||
fail("""
|
||||
A template \(modifiedTemplate) was modified, but the file \(generated) \
|
||||
was not regenerated.
|
||||
|
||||
Run `make gyb` from the root of the project and commit the changes.
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
// Fail if the committer modified a generated file.
|
||||
// A template should be modified instead.
|
||||
|
||||
for modifiedGeneratedFile in danger.git.modifiedFiles
|
||||
where modifiedGeneratedFile.contains("GENERATED-")
|
||||
{
|
||||
let template = modifiedGeneratedFile
|
||||
.replacingOccurrences(of: "GENERATED-", with: "") + ".gyb"
|
||||
|
||||
if !danger.git.modifiedFiles.contains(template) {
|
||||
fail("""
|
||||
A generated file \(modifiedGeneratedFile) was modified, but \
|
||||
the template it was generated from was not modified.
|
||||
|
||||
Please modify the template \(template) instead, \
|
||||
run `make gyb` from the root of the project and commit the changes.
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwiftLint.lint(inline: true,
|
||||
configFile: ".swiftlint.yml",
|
||||
strict: true,
|
||||
lintAllFiles: true)
|
||||
|
||||
if danger.warnings.isEmpty, danger.fails.isEmpty {
|
||||
markdown("LGTM")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
SWIFT_TEST_FLAGS=
|
||||
|
||||
debug:
|
||||
swift build -c debug
|
||||
|
||||
release:
|
||||
swift build -c release
|
||||
|
||||
test-debug:
|
||||
swift test -c debug $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-debug-sanitize-thread:
|
||||
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-release:
|
||||
swift test -c release $(SWIFT_TEST_FLAGS)
|
||||
|
||||
swift-version:
|
||||
swift -version
|
||||
|
||||
test-compatibility:
|
||||
swift test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
|
||||
|
||||
generate-compatibility-xcodeproj:
|
||||
swift package generate-xcodeproj --xcconfig-overrides Combine-Compatibility.xcconfig; \
|
||||
open OpenCombine.xcodeproj
|
||||
|
||||
generate-xcodeproj:
|
||||
swift package generate-xcodeproj --enable-code-coverage
|
||||
|
||||
gyb:
|
||||
$(shell ./utils/recursively_gyb.sh)
|
||||
|
||||
clean:
|
||||
rm -rf .build
|
||||
|
||||
.PHONY: debug release \
|
||||
test-debug \
|
||||
test-release \
|
||||
swift-version \
|
||||
test-compatibility-debug \
|
||||
generate-compatibility-xcodeproj \
|
||||
generate-xcodeproj \
|
||||
gyb \
|
||||
clean
|
||||
+10
-3
@@ -6,13 +6,20 @@ let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/GottaGoFast.git", from: "0.1.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "OpenCombine"),
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine", "GottaGoFast"])
|
||||
]
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"GottaGoFast"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://codecov.io/gh/broadwaylamb/OpenCombine)
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
|
||||
@@ -21,9 +22,30 @@ You can refer to [this gist](https://gist.github.com/broadwaylamb/c2c8550d76b3ff
|
||||
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
|
||||
|
||||
```
|
||||
$ swift test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
|
||||
$ make test-compatibility
|
||||
```
|
||||
|
||||
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
|
||||
|
||||
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/broadwaylamb/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
|
||||
|
||||
#### GYB
|
||||
|
||||
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
|
||||
different flavors in order to support several arities. For example, there are also `Publishers.MapKeyPath2`
|
||||
and `Publishers.MapKeyPath3`, which are very similar but different enough that Swift's type system
|
||||
can't help us here (because there's no support for variadic generics). Maintaining multiple instances of
|
||||
those generic types is tedious and error-prone (they can get out of sync), so we use the GYB tool for
|
||||
generating those instances from a template.
|
||||
|
||||
GYB is a Python script that evaluates Python code written inside a template file, so it's very flexible —
|
||||
templates can be arbitrarily complex. There is a good article about GYB on
|
||||
[NSHipster](https://nshipster.com/swift-gyb/).
|
||||
|
||||
GYB is part of the [Swift Open Source Project](https://github.com/apple/swift/blob/master/utils/gyb.py)
|
||||
and can be distributed under the same license as Swift itself.
|
||||
|
||||
GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift code from those
|
||||
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
|
||||
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
|
||||
should be regenerated using `make gyb`.
|
||||
|
||||
+45
-1416
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// COpenCombineHelpers.cpp
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23/09/2019.
|
||||
//
|
||||
|
||||
#include "COpenCombineHelpers.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <cstdlib>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <os/lock.h>
|
||||
#endif // __APPLE__
|
||||
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_BEGIN try {
|
||||
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_END } catch (...) { abort(); }
|
||||
|
||||
namespace {
|
||||
|
||||
static std::atomic<uint64_t> next_combine_identifier;
|
||||
|
||||
class PlatformIndependentMutex {
|
||||
public:
|
||||
virtual void lock() = 0;
|
||||
virtual void unlock() = 0;
|
||||
|
||||
virtual ~PlatformIndependentMutex() {}
|
||||
};
|
||||
|
||||
template <typename Mutex>
|
||||
class GenericMutex final : PlatformIndependentMutex {
|
||||
Mutex mutex_;
|
||||
public:
|
||||
void lock() override {
|
||||
mutex_.lock();
|
||||
}
|
||||
|
||||
void unlock() override {
|
||||
mutex_.unlock();
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef __APPLE__
|
||||
bool isOSUnfairLockAvailable() {
|
||||
// We're linking weakly, so if we're back-deploying, this will be null.
|
||||
return os_unfair_lock_lock != nullptr;
|
||||
}
|
||||
|
||||
template <>
|
||||
class GenericMutex<os_unfair_lock> final : PlatformIndependentMutex {
|
||||
os_unfair_lock mutex_ = OS_UNFAIR_LOCK_INIT;
|
||||
public:
|
||||
GenericMutex() = default;
|
||||
GenericMutex(const GenericMutex&) = delete;
|
||||
GenericMutex& operator=(const GenericMutex&) = delete;
|
||||
|
||||
void lock() override {
|
||||
os_unfair_lock_lock(&mutex_);
|
||||
}
|
||||
|
||||
void unlock() override {
|
||||
os_unfair_lock_unlock(&mutex_);
|
||||
}
|
||||
};
|
||||
#endif // __APPLE__
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void) {
|
||||
return next_combine_identifier.fetch_add(1);
|
||||
}
|
||||
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (isOSUnfairLockAvailable()) {
|
||||
return {new GenericMutex<os_unfair_lock>};
|
||||
} else {
|
||||
return {new GenericMutex<std::mutex>};
|
||||
}
|
||||
#else
|
||||
return {new GenericMutex<std::mutex>};
|
||||
#endif
|
||||
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
|
||||
return {new GenericMutex<std::recursive_mutex>};
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock lock) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(lock.opaque)->lock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock mutex) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(mutex.opaque)->unlock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock lock) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(lock.opaque)->lock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock mutex) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(mutex.opaque)->unlock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock) {
|
||||
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
|
||||
}
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock) {
|
||||
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// COpenCombineHelpers.h
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23/09/2019.
|
||||
//
|
||||
|
||||
#ifndef COPENCOMBINEHELPERS_H
|
||||
#define COPENCOMBINEHELPERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#if __has_attribute(swift_name)
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name) __attribute__((swift_name(#_name)))
|
||||
#else
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#pragma mark - CombineIdentifier
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void)
|
||||
OPENCOMBINE_SWIFT_NAME(nextCombineIdentifier());
|
||||
|
||||
#pragma mark - OpenCombineUnfairLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairLock) OpenCombineUnfairLock;
|
||||
|
||||
/// Allocates a lock object. The allocated object must be destroyed by calling
|
||||
/// the destroy() method.
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.allocate());
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.deallocate(self:));
|
||||
|
||||
#pragma mark - OpenCombineUnfairRecursiveLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairRecursiveLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.allocate());
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.deallocate(self:));
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* COPENCOMBINEHELPERS_H */
|
||||
@@ -10,6 +10,7 @@
|
||||
/// Subscriber implementations can use this type to provide a “cancellation token” that
|
||||
/// makes it possible for a caller to cancel a publisher, but not to use the
|
||||
/// `Subscription` object to request items.
|
||||
/// An AnyCancellable instance automatically calls `cancel()` when deinitialized.
|
||||
public final class AnyCancellable: Cancellable, Hashable {
|
||||
|
||||
private var _cancel: (() -> Void)?
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
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.
|
||||
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erasing publisher.
|
||||
///
|
||||
/// Use `AnyPublisher` to wrap a publisher whose type has details you don’t want to expose
|
||||
@@ -13,7 +24,6 @@ public struct AnyPublisher<Output, Failure: Error>
|
||||
: CustomStringConvertible,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
|
||||
@usableFromInline
|
||||
internal let box: PublisherBoxBase<Output, Failure>
|
||||
|
||||
@@ -47,8 +57,8 @@ extension AnyPublisher: Publisher {
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
@inlinable
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
box.subscribe(subscriber)
|
||||
}
|
||||
@@ -62,11 +72,11 @@ internal class PublisherBoxBase<Output, Failure: Error>: Publisher {
|
||||
@inlinable
|
||||
internal init() {}
|
||||
|
||||
@inlinable
|
||||
internal func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
@usableFromInline
|
||||
internal func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +94,8 @@ internal final class PublisherBox<PublisherType: Publisher>
|
||||
}
|
||||
|
||||
@inlinable
|
||||
override internal func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
override internal func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
base.subscribe(subscriber)
|
||||
}
|
||||
|
||||
@@ -137,17 +137,17 @@ internal class AnySubscriberBase<Input, Failure: Error>: Subscriber {
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(subscription: Subscription) {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(completion: Subscribers.Completion<Failure>) {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,52 +222,3 @@ internal final class ClosureBasedAnySubscriber<Input, Failure: Error>
|
||||
receiveCompletionThunk(completion)
|
||||
}
|
||||
}
|
||||
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
Subscription
|
||||
{
|
||||
internal var downstreamSubject: Downstream?
|
||||
internal var upstreamSubscription: Subscription?
|
||||
|
||||
internal init(_ parent: Downstream) {
|
||||
self.downstreamSubject = parent
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
guard upstreamSubscription == nil else { return }
|
||||
upstreamSubscription = subscription
|
||||
downstreamSubject?.send(subscription: self)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
|
||||
downstreamSubject?.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
downstreamSubject?.send(completion: completion)
|
||||
}
|
||||
|
||||
internal var description: String { return "Subject" }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children: [(label: String?, value: Any)] = [
|
||||
(label: "downstreamSubject", value: downstreamSubject as Any),
|
||||
(label: "upstreamSubscription", value: upstreamSubscription as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
downstreamSubject = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,35 +5,21 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.nextCombineIdentifier
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
@usableFromInline
|
||||
internal static var _counter: UInt = 0
|
||||
private let id: UInt64
|
||||
|
||||
@usableFromInline
|
||||
internal static var _counterLock = Lock(recursive: false)
|
||||
|
||||
@usableFromInline
|
||||
internal let _id: UInt
|
||||
|
||||
@inlinable
|
||||
public init() {
|
||||
|
||||
var id: UInt = 0
|
||||
|
||||
CombineIdentifier._counterLock.do {
|
||||
id = CombineIdentifier._counter
|
||||
CombineIdentifier._counter += 1
|
||||
}
|
||||
|
||||
_id = id
|
||||
self.id = nextCombineIdentifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
_id = UInt(bitPattern: ObjectIdentifier(obj))
|
||||
id = UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "0x\(String(_id, radix: 16))"
|
||||
return "0x\(String(id, radix: 16))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that wraps a single value and publishes a new element whenever the value
|
||||
/// changes.
|
||||
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = Lock(recursive: true)
|
||||
private let _lock = UnfairRecursiveLock.allocate()
|
||||
|
||||
// TODO: Combine uses bag data structure
|
||||
private var _subscriptions: [Conduit] = []
|
||||
|
||||
private var _value: Output
|
||||
|
||||
private var _completion: Subscribers.Completion<Failure>?
|
||||
|
||||
internal var upstreamSubscriptions: [Subscription] = []
|
||||
@@ -22,8 +26,11 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
/// The value wrapped by this subject, published as a new element whenever it changes.
|
||||
public var value: Output {
|
||||
didSet {
|
||||
send(value)
|
||||
get {
|
||||
return _value
|
||||
}
|
||||
set {
|
||||
send(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +38,14 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
///
|
||||
/// - Parameter value: The initial value to publish.
|
||||
public init(_ value: Output) {
|
||||
self.value = value
|
||||
self._value = value
|
||||
}
|
||||
|
||||
deinit {
|
||||
for subscription in _subscriptions {
|
||||
subscription._downstream = nil
|
||||
}
|
||||
_lock.deallocate()
|
||||
}
|
||||
|
||||
public func send(subscription: Subscription) {
|
||||
@@ -68,6 +76,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
public func send(_ input: Output) {
|
||||
_lock.do {
|
||||
_value = input
|
||||
for subscription in _subscriptions where !subscription.isCompleted {
|
||||
if subscription._demand > 0 {
|
||||
subscription._offer(input)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension UnfairLock {
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
lock()
|
||||
defer { unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
|
||||
extension UnfairRecursiveLock {
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
lock()
|
||||
defer { unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// PartialCompletion.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
/// A value of this type is returned by the overridden `receive(newValue:)` method
|
||||
/// of the `ReduceProducer` and `FilterProducer` classes.
|
||||
internal enum PartialCompletion<Value, Failure: Error> {
|
||||
|
||||
/// Indicate that we should continue accepting the upstream's output.
|
||||
case `continue`(Value)
|
||||
|
||||
/// Indicate that no values should be received from the upstream anymore.
|
||||
case finished
|
||||
|
||||
/// Indicate that there was a failure and we should send it downstream.
|
||||
case failure(Failure)
|
||||
}
|
||||
|
||||
extension PartialCompletion where Value == Void {
|
||||
|
||||
/// Indicate that we should continue accepting the upstream's output.
|
||||
internal static var `continue`: PartialCompletion { return .continue(()) }
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
//
|
||||
// ReduceProducer.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Reduce-like operators send an instance of their `Inner` class that is subclass
|
||||
/// of this class to the upstream publisher (as subscriber) and
|
||||
/// to the downstream subcriber (as subsription).
|
||||
///
|
||||
/// Reduce-like operators include `Publishers.Reduce`, `Publishers.TryReduce`,
|
||||
/// `Publishers.Count`, `Publishers.FirstWhere`, `Publishers.AllSatisfy` and more.
|
||||
///
|
||||
/// Subclasses must override the `receive(newValue:)` and `description`.
|
||||
internal class ReduceProducer<Downstream: Subscriber,
|
||||
Input,
|
||||
Output,
|
||||
UpstreamFailure: Error,
|
||||
Reducer>
|
||||
: CustomStringConvertible,
|
||||
CustomReflectable
|
||||
where Downstream.Input == Output
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
// MARK: - State
|
||||
|
||||
internal final var result: Output?
|
||||
|
||||
private let initial: Output?
|
||||
|
||||
internal final let reduce: Reducer
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var downstreamRequested = false
|
||||
|
||||
private var cancelled = false
|
||||
|
||||
private var completed = false
|
||||
|
||||
private var upstreamCompleted = false
|
||||
|
||||
private var empty = true
|
||||
|
||||
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
|
||||
self.downstream = downstream
|
||||
self.initial = initial
|
||||
self.result = initial
|
||||
self.reduce = reduce
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
// MARK: - Abstract methods
|
||||
|
||||
internal func receive(
|
||||
newValue: Input
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
internal var description: String {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
// MARK: - CustomReflectable
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("result", result as Any),
|
||||
("initial", initial as Any),
|
||||
("status", status)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// - Precondition: `lock` is held.
|
||||
private func receiveFinished() {
|
||||
guard !cancelled, !completed, !upstreamCompleted else {
|
||||
lock.unlock()
|
||||
// This should never happen, because `receive(completion:)`
|
||||
// (from which this function is called) early exists if
|
||||
// `status` is `.terminal`.
|
||||
assertionFailure("The subscription should have been terminated by now")
|
||||
return
|
||||
}
|
||||
upstreamCompleted = true
|
||||
self.completed = downstreamRequested || empty
|
||||
let completed = self.completed
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
|
||||
if completed {
|
||||
sendResultAndFinish(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Precondition: `lock` is held.
|
||||
private func receiveFailure(_ failure: UpstreamFailure) {
|
||||
guard !cancelled, !completed, !upstreamCompleted else {
|
||||
lock.unlock()
|
||||
// This should never happen, because `receive(completion:)`
|
||||
// (from which this function is called) early exists if
|
||||
// `status` is `.terminal`.
|
||||
assertionFailure("The subscription should have been terminated by now")
|
||||
return
|
||||
}
|
||||
upstreamCompleted = true
|
||||
completed = true
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .failure(failure as! Downstream.Failure))
|
||||
}
|
||||
|
||||
private func sendResultAndFinish(_ result: Output?) {
|
||||
assert(completed && upstreamCompleted)
|
||||
if let result = result {
|
||||
_ = downstream.receive(result)
|
||||
}
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
}
|
||||
|
||||
extension ReduceProducer: Subscriber {
|
||||
internal func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
empty = false
|
||||
lock.unlock()
|
||||
|
||||
// Combine doesn't hold the lock when calling `receive(newValue:)`.
|
||||
//
|
||||
// This can lead to data races if the contract is violated
|
||||
// (like when we receive input from multiple threads simultaneously).
|
||||
switch self.receive(newValue: input) {
|
||||
case .continue:
|
||||
break
|
||||
case .finished:
|
||||
lock.lock()
|
||||
upstreamCompleted = true
|
||||
let downstreamRequested = self.downstreamRequested
|
||||
if downstreamRequested {
|
||||
completed = true
|
||||
}
|
||||
status = .terminal
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
|
||||
subscription.cancel()
|
||||
|
||||
guard downstreamRequested else { break }
|
||||
|
||||
sendResultAndFinish(result)
|
||||
case let .failure(error):
|
||||
lock.lock()
|
||||
upstreamCompleted = true
|
||||
completed = true
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<UpstreamFailure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
switch completion {
|
||||
case .finished:
|
||||
receiveFinished()
|
||||
case let .failure(error):
|
||||
receiveFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReduceProducer: Subscription {
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard !downstreamRequested, !cancelled, !completed else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstreamRequested = true
|
||||
guard upstreamCompleted else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
completed = true
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
sendResultAndFinish(result)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension ReduceProducer: CustomPlaygroundDisplayConvertible {
|
||||
internal var playgroundDescription: Any { return description }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// SubjectSubscriber.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible,
|
||||
Subscription
|
||||
{
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var downstreamSubject: Downstream?
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var isCancelled: Bool { return downstreamSubject == nil }
|
||||
|
||||
internal init(_ parent: Downstream) {
|
||||
self.downstreamSubject = parent
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstreamSubscription == nil, let subject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
lock.unlock()
|
||||
subject.send(subscription: self)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard let downstreamSubject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationValueBeforeSubscription() }
|
||||
lock.unlock()
|
||||
downstreamSubject.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
lock.lock()
|
||||
guard let subject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationUnexpectedCompletion() }
|
||||
lock.unlock()
|
||||
subject.send(completion: completion)
|
||||
downstreamSubject = nil
|
||||
}
|
||||
|
||||
internal var description: String { return "Subject" }
|
||||
|
||||
internal var playgroundDescription: Any { return description }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstreamSubject", downstreamSubject as Any),
|
||||
("upstreamSubscription", upstreamSubscription as Any),
|
||||
("subject", downstreamSubject as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard let subscription = upstreamSubscription else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
if isCancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard let subscription = upstreamSubscription else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = nil
|
||||
downstreamSubject = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// SubscriptionStatus.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21.09.2019.
|
||||
//
|
||||
|
||||
internal enum SubscriptionStatus {
|
||||
case awaitingSubscription
|
||||
case subscribed(Subscription)
|
||||
case terminal
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Violations.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
internal func APIViolationValueBeforeSubscription(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("""
|
||||
API Violation: received an unexpected value before receiving a Subscription
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
internal func APIViolationUnexpectedCompletion(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("API Violation: received an unexpected completion", file: file, line: line)
|
||||
}
|
||||
|
||||
internal func abstractMethod(file: StaticString = #file, line: UInt = #line) -> Never {
|
||||
fatalError("Abstract method call", file: file, line: line)
|
||||
}
|
||||
|
||||
extension Subscribers.Demand {
|
||||
internal func assertNonZero(file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
if self == .none {
|
||||
fatalError("API Violation: demand must not be zero", file: file, line: line)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,9 +139,7 @@ public struct ImmediateScheduler: Scheduler {
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
fatalError(
|
||||
"Attempt to schedule something in the future on the immediate scheduler"
|
||||
)
|
||||
action()
|
||||
}
|
||||
|
||||
/// Performs the action at some time after the specified date, at the specified
|
||||
@@ -151,8 +149,7 @@ public struct ImmediateScheduler: Scheduler {
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
fatalError(
|
||||
"Attempt to schedule something in the future on the immediate scheduler"
|
||||
)
|
||||
action()
|
||||
return Subscriptions.empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("How to do locking on this platform?")
|
||||
#endif
|
||||
|
||||
@usableFromInline
|
||||
internal final class Lock {
|
||||
|
||||
@usableFromInline
|
||||
internal var _mutex = pthread_mutex_t()
|
||||
|
||||
@inlinable
|
||||
internal init(recursive: Bool) {
|
||||
var attrib = pthread_mutexattr_t()
|
||||
pthread_mutexattr_init(&attrib)
|
||||
if recursive {
|
||||
pthread_mutexattr_settype(&attrib, Int32(PTHREAD_MUTEX_RECURSIVE))
|
||||
}
|
||||
pthread_mutex_init(&_mutex, &attrib)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
deinit {
|
||||
pthread_mutex_destroy(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func _lock() {
|
||||
pthread_mutex_lock(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func _unlock() {
|
||||
pthread_mutex_unlock(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
_lock()
|
||||
defer { _unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,15 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that passes along values and completion.
|
||||
///
|
||||
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
|
||||
/// specific values on-demand during tests.
|
||||
public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = Lock(recursive: true)
|
||||
private let _lock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private var _completion: Subscribers.Completion<Failure>?
|
||||
|
||||
@@ -28,6 +30,7 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
for subscription in _subscriptions {
|
||||
subscription._downstream = nil
|
||||
}
|
||||
_lock.deallocate()
|
||||
}
|
||||
|
||||
public func send(subscription: Subscription) {
|
||||
@@ -39,8 +42,8 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
}
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
_lock.do {
|
||||
if let completion = _completion {
|
||||
@@ -119,7 +122,7 @@ extension PassthroughSubject {
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
precondition(demand > 0, "demand must not be zero")
|
||||
_parent?._lock.do {
|
||||
_demand = demand
|
||||
_demand += demand
|
||||
}
|
||||
_parent?._acknowledgeDownstreamDemand()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// Published.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Евгений Богомолов on 01/09/2019.
|
||||
//
|
||||
|
||||
#if swift(>=5.1)
|
||||
/// Adds a `Publisher` to a property.
|
||||
///
|
||||
/// Properties annotated with `@Published` contain both the stored value
|
||||
/// and a publisher which sends any new values after the property value
|
||||
/// has been sent. New subscribers will receive the current value
|
||||
/// of the property first.
|
||||
/// Note that the `@Published` property is class-constrained.
|
||||
/// Use it with properties of classes, not with non-class types like structures.
|
||||
@propertyWrapper public struct Published<Value> {
|
||||
|
||||
/// Initialize the storage of the Published
|
||||
/// property as well as the corresponding `Publisher`.
|
||||
public init(initialValue: Value) {
|
||||
value = initialValue
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
public init(wrappedValue: Value) {
|
||||
value = wrappedValue
|
||||
}
|
||||
|
||||
/// A publisher for properties marked with the `@Published` attribute.
|
||||
public struct Publisher: OpenCombine.Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Value
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Never
|
||||
|
||||
/// 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.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Value, Downstream.Failure == Never
|
||||
{
|
||||
subject.subscribe(subscriber)
|
||||
}
|
||||
|
||||
fileprivate let subject: OpenCombine.CurrentValueSubject<Value, Never>
|
||||
|
||||
fileprivate init(_ output: Output) {
|
||||
subject = .init(output)
|
||||
}
|
||||
}
|
||||
|
||||
private var value: Value
|
||||
|
||||
/// The property that can be accessed with the
|
||||
/// `$` syntax and allows access to the `Publisher`
|
||||
public var projectedValue: Publisher {
|
||||
mutating get {
|
||||
if let publisher = publisher {
|
||||
return publisher
|
||||
}
|
||||
let publisher = Publisher(value)
|
||||
self.publisher = publisher
|
||||
return publisher
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable, message:
|
||||
"@Published is only available on properties of classes")
|
||||
|
||||
public var wrappedValue: Value {
|
||||
get { value }
|
||||
set {
|
||||
value = newValue
|
||||
publisher?.subject.value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var publisher: Publisher?
|
||||
|
||||
@available(*, unavailable, message:
|
||||
"This subscript is unavailable in OpenCombine yet")
|
||||
public static subscript<EnclosingSelf: AnyObject>(
|
||||
_enclosingInstance object: EnclosingSelf,
|
||||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
|
||||
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Published<Value>>
|
||||
) -> Value {
|
||||
get { fatalError() }
|
||||
set { fatalError() }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -38,9 +38,9 @@ public struct Deferred<DeferredPublisher: Publisher>: Publisher {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure,
|
||||
Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
{
|
||||
let deferredPublisher = createPublisher()
|
||||
deferredPublisher.subscribe(subscriber)
|
||||
|
||||
@@ -42,8 +42,8 @@ public struct Empty<Output, Failure: Error>: Publisher, Equatable {
|
||||
/// to the subscriber. If `false`, it never completes.
|
||||
public let completeImmediately: Bool
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
if completeImmediately {
|
||||
|
||||
@@ -30,8 +30,8 @@ public struct Fail<Output, Failure: Error>: Publisher {
|
||||
/// The failure to send when terminating the publisher.
|
||||
public let error: Failure
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
subscriber.receive(completion: .failure(error))
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publishers.MapKeyPath.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Returns a publisher that publishes the values of a keyt path as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath: The key path of a property on `Output`
|
||||
/// - Returns: A publisher that publishes the value of the key path.
|
||||
public func map<Result>(
|
||||
_ keyPath: KeyPath<Output, Result>
|
||||
) -> Publishers.MapKeyPath<Self, Result> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
keyPath: keyPath
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a publisher that publishes the values of two key paths as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath0: The key path of a property on `Output`
|
||||
/// - keyPath1: The key path of another property on `Output`
|
||||
/// - Returns: A publisher that publishes the values of two key paths as a tuple.
|
||||
public func map<Result0, Result1>(
|
||||
_ keyPath0: KeyPath<Output, Result0>,
|
||||
_ keyPath1: KeyPath<Output, Result1>
|
||||
) -> Publishers.MapKeyPath2<Self, Result0, Result1> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
keyPath0: keyPath0,
|
||||
keyPath1: keyPath1
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a publisher that publishes the values of three key paths as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath0: The key path of a property on `Output`
|
||||
/// - keyPath1: The key path of another property on `Output`
|
||||
/// - keyPath2: The key path of a third property on `Output`
|
||||
/// - Returns: A publisher that publishes the values of three key paths as a tuple.
|
||||
public func map<Result0, Result1, Result2>(
|
||||
_ keyPath0: KeyPath<Output, Result0>,
|
||||
_ keyPath1: KeyPath<Output, Result1>,
|
||||
_ keyPath2: KeyPath<Output, Result2>
|
||||
) -> Publishers.MapKeyPath3<Self, Result0, Result1, Result2> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
keyPath0: keyPath0,
|
||||
keyPath1: keyPath1,
|
||||
keyPath2: keyPath2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes the value of a key path.
|
||||
public struct MapKeyPath<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The key path of a property to publish.
|
||||
public let keyPath: KeyPath<Upstream.Output, Output>
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that publishes the values of two key paths as a tuple.
|
||||
public struct MapKeyPath2<Upstream: Publisher, Output0, Output1>: Publisher {
|
||||
|
||||
public typealias Output = (Output0, Output1)
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The key path of a property to publish.
|
||||
public let keyPath0: KeyPath<Upstream.Output, Output0>
|
||||
|
||||
/// The key path of a second property to publish.
|
||||
public let keyPath1: KeyPath<Upstream.Output, Output1>
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that publishes the values of three key paths as a tuple.
|
||||
public struct MapKeyPath3<Upstream: Publisher, Output0, Output1, Output2>: Publisher {
|
||||
|
||||
public typealias Output = (Output0, Output1, Output2)
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The key path of a property to publish.
|
||||
public let keyPath0: KeyPath<Upstream.Output, Output0>
|
||||
|
||||
/// The key path of a second property to publish.
|
||||
public let keyPath1: KeyPath<Upstream.Output, Output1>
|
||||
|
||||
/// The key path of a third property to publish.
|
||||
public let keyPath2: KeyPath<Upstream.Output, Output2>
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MapKeyPath {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let keyPath: KeyPath<Input, Output>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.MapKeyPath<Upstream, Output>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.keyPath = parent.keyPath
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let output = (
|
||||
input[keyPath: keyPath]
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "ValueForKey" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("keyPath", keyPath),
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MapKeyPath2 {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let keyPath0: KeyPath<Input, Output0>
|
||||
|
||||
private let keyPath1: KeyPath<Input, Output1>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.MapKeyPath2<Upstream, Output0, Output1>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.keyPath0 = parent.keyPath0
|
||||
self.keyPath1 = parent.keyPath1
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let output = (
|
||||
input[keyPath: keyPath0],
|
||||
input[keyPath: keyPath1]
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "ValueForKeys" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("keyPath0", keyPath0),
|
||||
("keyPath1", keyPath1),
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MapKeyPath3 {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let keyPath0: KeyPath<Input, Output0>
|
||||
|
||||
private let keyPath1: KeyPath<Input, Output1>
|
||||
|
||||
private let keyPath2: KeyPath<Input, Output2>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.MapKeyPath3<Upstream, Output0, Output1, Output2>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.keyPath0 = parent.keyPath0
|
||||
self.keyPath1 = parent.keyPath1
|
||||
self.keyPath2 = parent.keyPath2
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let output = (
|
||||
input[keyPath: keyPath0],
|
||||
input[keyPath: keyPath1],
|
||||
input[keyPath: keyPath2]
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "ValueForKeys" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("keyPath0", keyPath0),
|
||||
("keyPath1", keyPath1),
|
||||
("keyPath2", keyPath2),
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ public struct Just<Output>: Publisher {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where SubscriberType.Input == Output, SubscriberType.Failure == Never
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Never
|
||||
{
|
||||
subscriber.receive(subscription: Inner(value: output, downstream: subscriber))
|
||||
}
|
||||
@@ -251,33 +251,43 @@ extension Just {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Just {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let value: Output
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
fileprivate init(value: Output, downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(value)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Just" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Just" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(value))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ extension Optional {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
if let output = output {
|
||||
subscriber.receive(subscription: Inner(value: output,
|
||||
@@ -74,34 +74,44 @@ extension Optional {
|
||||
#endif
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Optional.OCombine {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Wrapped
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let output: Wrapped
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
init(value: Wrapped, downstream: Downstream) {
|
||||
self.output = value
|
||||
self.downstream = downstream
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Optional" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Optional" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(output))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +148,7 @@ extension Optional.OCombine.Publisher {
|
||||
}
|
||||
|
||||
public func collect() -> Optional<[Output]>.OCombine.Publisher {
|
||||
return .init(self.output.map { [$0] })
|
||||
return .init(self.output.map { [$0] } ?? [])
|
||||
}
|
||||
|
||||
public func compactMap<ElementOfResult>(
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// Publishers.AllSatisfy.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes a single Boolean value that indicates whether all received elements pass
|
||||
/// a given predicate.
|
||||
///
|
||||
/// When this publisher receives an element, it runs the predicate against
|
||||
/// the element. If the predicate returns `false`, the publisher produces a `false`
|
||||
/// value and finishes. If the upstream publisher finishes normally, this publisher
|
||||
/// produces a `true` value and finishes.
|
||||
/// As a `reduce`-style operator, this publisher produces at most one value.
|
||||
/// Backpressure note: Upon receiving any request greater than zero, this publisher
|
||||
/// requests unlimited elements from the upstream publisher.
|
||||
///
|
||||
/// - Parameter predicate: A closure that evaluates each received element.
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and complete.
|
||||
/// - Returns: A publisher that publishes a Boolean value that indicates whether
|
||||
/// all received elements pass a given predicate.
|
||||
public func allSatisfy(
|
||||
_ predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.AllSatisfy<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Publishes a single Boolean value that indicates whether all received elements pass
|
||||
/// a given error-throwing predicate.
|
||||
///
|
||||
/// When this publisher receives an element, it runs the predicate against
|
||||
/// the element. If the predicate returns `false`, the publisher produces a `false`
|
||||
/// value and finishes. If the upstream publisher finishes normally, this publisher
|
||||
/// produces a `true` value and finishes. If the predicate throws an error,
|
||||
/// the publisher fails, passing the error to its downstream.
|
||||
/// As a `reduce`-style operator, this publisher produces at most one value.
|
||||
/// Backpressure note: Upon receiving any request greater than zero, this publisher
|
||||
/// requests unlimited elements from the upstream publisher.
|
||||
///
|
||||
/// - Parameter predicate: A closure that evaluates each received element.
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and complete.
|
||||
/// The closure may throw, in which case the publisher cancels the upstream
|
||||
/// publisher and fails with the thrown error.
|
||||
/// - Returns: A publisher that publishes a Boolean value that indicates whether
|
||||
/// all received elements pass a given predicate.
|
||||
public func tryAllSatisfy(
|
||||
_ predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryAllSatisfy<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes a single Boolean value that indicates whether
|
||||
/// all received elements pass a given predicate.
|
||||
public struct AllSatisfy<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that evaluates each received element.
|
||||
///
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and finish.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that publishes a single Boolean value that indicates whether
|
||||
/// all received elements pass a given error-throwing predicate.
|
||||
public struct TryAllSatisfy<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that evaluates each received element.
|
||||
///
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and complete.
|
||||
/// The closure may throw, in which case the publisher cancels the upstream
|
||||
/// publisher and fails with the thrown error.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Error, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.AllSatisfy {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Downstream.Input == Output, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: true, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if !reduce(newValue) {
|
||||
result = false
|
||||
return .finished
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "AllSatisfy" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryAllSatisfy {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: true, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if try !reduce(newValue) {
|
||||
result = false
|
||||
return .finished
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "TryAllSatisfy" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//
|
||||
// Publishers.Autoconnect.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable
|
||||
/// publisher.
|
||||
///
|
||||
/// Use `autoconnect()` to simplify working with `ConnectablePublisher` instances,
|
||||
/// such as those created with `makeConnectable()`.
|
||||
///
|
||||
/// let autoconnectedPublisher = somePublisher
|
||||
/// .makeConnectable()
|
||||
/// .autoconnect()
|
||||
/// .subscribe(someSubscriber)
|
||||
///
|
||||
/// - Returns: A publisher which automatically connects to its upstream connectable
|
||||
/// publisher.
|
||||
public func autoconnect() -> Publishers.Autoconnect<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that automatically connects and disconnects from this connectable
|
||||
/// publisher.
|
||||
public class Autoconnect<Upstream: ConnectablePublisher>: Publisher {
|
||||
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case disconnected
|
||||
case connected(refcount: Int, connection: Cancellable)
|
||||
}
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public final let upstream: Upstream
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state = State.disconnected
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(parent: self, downstream: subscriber)
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .connected(refcount, connection):
|
||||
state = .connected(refcount: refcount + 1, connection: connection)
|
||||
lock.unlock()
|
||||
upstream.subscribe(inner)
|
||||
case .disconnected:
|
||||
lock.unlock()
|
||||
upstream.subscribe(inner)
|
||||
let connection = upstream.connect()
|
||||
lock.lock()
|
||||
state = .connected(refcount: 1, connection: connection)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func willCancel() {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .connected(refcount, connection):
|
||||
if refcount <= 1 {
|
||||
self.state = .disconnected
|
||||
lock.unlock()
|
||||
connection.cancel()
|
||||
} else {
|
||||
state = .connected(refcount: refcount - 1, connection: connection)
|
||||
lock.unlock()
|
||||
}
|
||||
case .disconnected:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Autoconnect {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate let combineIdentifier = CombineIdentifier()
|
||||
|
||||
private let parent: Publishers.Autoconnect<Upstream>
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
fileprivate init(parent: Publishers.Autoconnect<Upstream>,
|
||||
downstream: Downstream) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
let sideEffectSubscription = SideEffectSubscription(subscription,
|
||||
parent: parent)
|
||||
downstream.receive(subscription: sideEffectSubscription)
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "Autoconnect" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("parent", parent),
|
||||
("downstream", downstream)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
private struct SideEffectSubscription
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private let parent: Publishers.Autoconnect<Upstream>
|
||||
|
||||
private let upstreamSubscription: Subscription
|
||||
|
||||
fileprivate init(_ upstreamSubscription: Subscription,
|
||||
parent: Publishers.Autoconnect<Upstream>) {
|
||||
self.parent = parent
|
||||
self.upstreamSubscription = upstreamSubscription
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription.request(demand)
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
parent.willCancel()
|
||||
upstreamSubscription.cancel()
|
||||
}
|
||||
|
||||
fileprivate var combineIdentifier: CombineIdentifier {
|
||||
return upstreamSubscription.combineIdentifier
|
||||
}
|
||||
|
||||
fileprivate var description: String {
|
||||
return String(describing: upstreamSubscription)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any {
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Publishers.Collect.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Collects all received elements, and emits a single array of the collection when
|
||||
/// the upstream publisher finishes.
|
||||
///
|
||||
/// If the upstream publisher fails with an error, this publisher forwards the error
|
||||
/// to the downstream receiver instead of sending its output.
|
||||
/// This publisher requests an unlimited number of elements from the upstream
|
||||
/// publisher. It only sends the collected array to its downstream after a request
|
||||
/// whose demand is greater than 0 items.
|
||||
/// Note: This publisher uses an unbounded amount of memory to store the received
|
||||
/// values.
|
||||
///
|
||||
/// - Returns: A publisher that collects all received items and returns them as
|
||||
/// an array upon completion.
|
||||
public func collect() -> Publishers.Collect<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that buffers items.
|
||||
public struct Collect<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = [Upstream.Output]
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == [Upstream.Output]
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Collect: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Collect {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
[Upstream.Output],
|
||||
Upstream.Failure,
|
||||
Void>
|
||||
where Downstream.Input == [Upstream.Output],
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: [], reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result!.append(newValue)
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "Collect"
|
||||
}
|
||||
|
||||
override var customMirror: Mirror {
|
||||
let children: CollectionOfOne<Mirror.Child> = .init(("count", result!.count))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// Publishers.CompactMap.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.07.2019.
|
||||
//
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling a closure
|
||||
/// with each received element.
|
||||
public struct CompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
public let transform: (Upstream.Output) -> Output?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling an error-throwing
|
||||
/// closure with each received element.
|
||||
public struct TryCompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// An error-throwing closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
///
|
||||
/// If this closure throws an error, the publisher fails.
|
||||
public let transform: (Upstream.Output) throws -> Output?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) throws -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Calls a closure with each received element and publishes any returned
|
||||
/// optional that has a value.
|
||||
///
|
||||
/// - Parameter transform: A closure that receives a value and returns
|
||||
/// an optional value.
|
||||
/// - Returns: A publisher that republishes all non-`nil` results of calling
|
||||
/// the transform closure.
|
||||
public func compactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) -> ElementOfResult?
|
||||
) -> Publishers.CompactMap<Self, ElementOfResult> {
|
||||
return .init(upstream: self, transform: transform)
|
||||
}
|
||||
|
||||
/// Calls an error-throwing closure with each received element and publishes
|
||||
/// any returned optional that has a value.
|
||||
///
|
||||
/// If the closure throws an error, the publisher cancels the upstream and sends
|
||||
/// the thrown error to the downstream receiver as a `Failure`.
|
||||
///
|
||||
/// - Parameter transform: an error-throwing closure that receives a value
|
||||
/// and returns an optional value.
|
||||
/// - Returns: A publisher that republishes all non-`nil` results of calling
|
||||
/// the `transform` closure.
|
||||
public func tryCompactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) throws -> ElementOfResult?
|
||||
) -> Publishers.TryCompactMap<Self, ElementOfResult> {
|
||||
return .init(upstream: self, transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CompactMap {
|
||||
|
||||
public func compactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) -> ElementOfResult?
|
||||
) -> Publishers.CompactMap<Upstream, ElementOfResult> {
|
||||
return .init(upstream: upstream,
|
||||
transform: { self.transform($0).flatMap(transform) })
|
||||
}
|
||||
|
||||
public func map<ElementOfResult>(
|
||||
_ transform: @escaping (Output) -> ElementOfResult
|
||||
) -> Publishers.CompactMap<Upstream, ElementOfResult> {
|
||||
return .init(upstream: upstream,
|
||||
transform: { self.transform($0).map(transform) })
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryCompactMap {
|
||||
|
||||
public func compactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) throws -> ElementOfResult?
|
||||
) -> Publishers.TryCompactMap<Upstream, ElementOfResult> {
|
||||
return .init(upstream: upstream,
|
||||
transform: { try self.transform($0).flatMap(transform) })
|
||||
}
|
||||
}
|
||||
|
||||
private class _CompactMap<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Transform = (Input) -> Result<Downstream.Input?, Downstream.Failure>
|
||||
|
||||
fileprivate var _transform: Transform?
|
||||
|
||||
var _isCompleted: Bool {
|
||||
return _transform == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, transform: @escaping Transform) {
|
||||
_transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
guard let transform = _transform else { return .none }
|
||||
|
||||
switch transform(input) {
|
||||
case .success(let output?):
|
||||
return downstream.receive(output)
|
||||
case .success(nil):
|
||||
return .max(1)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
_transform = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard !_isCompleted else { return }
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_transform = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CompactMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _CompactMap<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
var description: String { return "CompactMap" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if !_isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryCompactMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _CompactMap<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Downstream.Failure == Error
|
||||
{
|
||||
var description: String { return "TryCompactMap" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if !_isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// Publishers.Comparison.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Ilija Puaca on 22/7/19.
|
||||
//
|
||||
|
||||
extension Publisher where Output: Comparable {
|
||||
|
||||
/// Publishes the minimum value received from the upstream publisher, after it
|
||||
/// finishes.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that publishes the minimum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func min() -> Publishers.Comparison<Self> {
|
||||
return max(by: >)
|
||||
}
|
||||
|
||||
/// Publishes the maximum value received from the upstream publisher, after it
|
||||
/// finishes.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that publishes the maximum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func max() -> Publishers.Comparison<Self> {
|
||||
return max(by: <)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes the minimum value received from the upstream publisher, after it
|
||||
/// finishes.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A closure that receives two elements and returns
|
||||
/// `true` if they are in increasing order.
|
||||
/// - Returns: A publisher that publishes the minimum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func min(
|
||||
by areInIncreasingOrder: @escaping (Output, Output) -> Bool
|
||||
) -> Publishers.Comparison<Self> {
|
||||
return max(by: { areInIncreasingOrder($1, $0) })
|
||||
}
|
||||
|
||||
/// Publishes the minimum value received from the upstream publisher, using the
|
||||
/// provided error-throwing closure to order the items.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A throwing closure that receives two elements
|
||||
/// and returns `true` if they are in increasing order. If this closure throws, the
|
||||
/// publisher terminates with a `Failure`.
|
||||
/// - Returns: A publisher that publishes the minimum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func tryMin(
|
||||
by areInIncreasingOrder: @escaping (Output, Output) throws -> Bool
|
||||
) -> Publishers.TryComparison<Self> {
|
||||
return tryMax(by: { try areInIncreasingOrder($1, $0) })
|
||||
}
|
||||
|
||||
/// Publishes the maximum value received from the upstream publisher, using the
|
||||
/// provided ordering closure.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A closure that receives two elements and returns
|
||||
/// `true` if they are in increasing order.
|
||||
/// - Returns: A publisher that publishes the maximum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func max(
|
||||
by areInIncreasingOrder: @escaping (Output, Output) -> Bool
|
||||
) -> Publishers.Comparison<Self> {
|
||||
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
/// Publishes the maximum value received from the upstream publisher, using the
|
||||
/// provided error-throwing closure to order the items.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
/// - Parameter areInIncreasingOrder: A throwing closure that receives two elements
|
||||
/// and returns `true` if they are in increasing order. If this closure throws, the
|
||||
/// publisher terminates with a `Failure`.
|
||||
/// - Returns: A publisher that publishes the maximum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func tryMax(
|
||||
by areInIncreasingOrder: @escaping (Self.Output, Self.Output) throws -> Bool
|
||||
) -> Publishers.TryComparison<Self> {
|
||||
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes items from another publisher only if each new item is
|
||||
/// in increasing order from the previously-published item.
|
||||
public struct Comparison<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives two elements and returns `true` if they are in
|
||||
/// increasing order.
|
||||
public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) -> Bool
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.areInIncreasingOrder = areInIncreasingOrder
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
areInIncreasingOrder: areInIncreasingOrder)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes items from another publisher only if each new item is
|
||||
/// in increasing order from the previously-published item, and fails if the ordering
|
||||
/// logic throws an error.
|
||||
public struct TryComparison<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives two elements and returns `true` if they are in
|
||||
/// increasing order.
|
||||
public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) throws -> Bool
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
areInIncreasingOrder:
|
||||
@escaping (Upstream.Output, Upstream.Output) throws -> Bool
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.areInIncreasingOrder = areInIncreasingOrder
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
areInIncreasingOrder: areInIncreasingOrder)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Comparison {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output, Upstream.Output) -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool
|
||||
) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if let result = self.result {
|
||||
if reduce(result, newValue) {
|
||||
self.result = newValue
|
||||
}
|
||||
} else {
|
||||
self.result = newValue
|
||||
}
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "Comparison"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryComparison {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output, Upstream.Output) throws -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Error
|
||||
{
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
areInIncreasingOrder:
|
||||
@escaping (Upstream.Output, Upstream.Output) throws -> Bool
|
||||
) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if let result = self.result {
|
||||
if try reduce(result, newValue) {
|
||||
self.result = newValue
|
||||
}
|
||||
} else {
|
||||
self.result = newValue
|
||||
}
|
||||
return .continue
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "TryComparison"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// Publishers.Contains.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher where Output: Equatable {
|
||||
|
||||
/// Publishes a Boolean value upon receiving an element equal to the argument.
|
||||
///
|
||||
/// The contains publisher consumes all received elements until the upstream publisher
|
||||
/// produces a matching element. At that point, it emits `true` and finishes normally.
|
||||
/// If the upstream finishes normally without producing a matching element,
|
||||
/// this publisher emits `false`, then finishes.
|
||||
///
|
||||
/// - Parameter output: An element to match against.
|
||||
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
|
||||
/// publisher emits a matching value.
|
||||
public func contains(_ output: Output) -> Publishers.Contains<Self> {
|
||||
return .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes a Boolean value upon receiving an element that satisfies the predicate
|
||||
/// closure.
|
||||
///
|
||||
/// This operator consumes elements produced from the upstream publisher until
|
||||
/// the upstream publisher produces a matching element.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether the element satisfies the closure’s
|
||||
/// comparison logic.
|
||||
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
|
||||
/// publisher emits a matching value.
|
||||
public func contains(
|
||||
where predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.ContainsWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Publishes a Boolean value upon receiving an element that satisfies
|
||||
/// the throwing predicate closure.
|
||||
///
|
||||
/// This operator consumes elements produced from the upstream publisher until
|
||||
/// the upstream publisher produces a matching element. If the closure throws,
|
||||
/// the stream fails with an error.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether the element satisfies the closure’s
|
||||
/// comparison logic.
|
||||
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
|
||||
/// publisher emits a matching value.
|
||||
public func tryContains(
|
||||
where predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryContainsWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits a Boolean value when a specified element is received from
|
||||
/// its upstream publisher.
|
||||
public struct Contains<Upstream: Publisher>: Publisher
|
||||
where Upstream.Output: Equatable
|
||||
{
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The element to scan for in the upstream publisher.
|
||||
public let output: Upstream.Output
|
||||
|
||||
public init(upstream: Upstream, output: Upstream.Output) {
|
||||
self.upstream = upstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that emits a Boolean value upon receiving an element that satisfies
|
||||
/// the predicate closure.
|
||||
public struct ContainsWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that determines whether the publisher should consider an element
|
||||
/// as a match.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that emits a Boolean value upon receiving an element that satisfies
|
||||
/// the throwing predicate closure.
|
||||
public struct TryContainsWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether this publisher should
|
||||
/// emit a `true` element.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Error, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Contains {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream, Upstream.Output, Bool, Upstream.Failure, Void>
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
private let output: Upstream.Output
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Upstream.Output) {
|
||||
self.output = output
|
||||
super.init(downstream: downstream, initial: false, reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if newValue == output {
|
||||
result = true
|
||||
return .finished
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Contains" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Contains : Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.ContainsWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output, Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: false, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if reduce(newValue) {
|
||||
result = true
|
||||
return .finished
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "ContainsWhere" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryContainsWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output, Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Downstream.Failure == Error, Downstream.Input == Bool
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: false, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if try reduce(newValue) {
|
||||
result = true
|
||||
return .finished
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "TryContainsWhere" }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,17 @@
|
||||
// Created by Joseph Spadafora on 6/25/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes the number of elements received from the upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that consumes all elements until the upstream publisher
|
||||
/// finishes, then emits a single value with the total number of elements received.
|
||||
public func count() -> Publishers.Count<Self> {
|
||||
return Publishers.Count(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes the number of elements received
|
||||
@@ -33,62 +44,34 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Upstream.Failure == SubscriberType.Failure,
|
||||
SubscriberType.Input == Output
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
let count = _Count<Upstream, SubscriberType>(downstream: subscriber)
|
||||
upstream.subscribe(count)
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
extension Publishers.Count: Equatable where Upstream: Equatable {}
|
||||
|
||||
/// Publishes the number of elements received from the upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that consumes all elements until the upstream publisher
|
||||
/// finishes, then emits a single value with the total number of elements received.
|
||||
public func count() -> Publishers.Count<Self> {
|
||||
return Publishers.Count(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
private final class _Count<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Downstream.Input == Int,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Output = Int
|
||||
typealias Failure = Downstream.Failure
|
||||
|
||||
private var _count = 0
|
||||
|
||||
var description: String { return "Count" }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
upstreamSubscription?.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
_count += 1
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if case .finished = completion {
|
||||
_ = downstream.receive(_count)
|
||||
extension Publishers.Count {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream, Upstream.Output, Int, Failure, Void>
|
||||
where Downstream.Input == Int,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: 0, reduce: ())
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result! += 1
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Count" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let decodeSubscriber = _Decode<Upstream, SubscriberType, Coder>(
|
||||
let decodeSubscriber = _Decode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
decoder: _decoder
|
||||
)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// Publishers.Drop.swift
|
||||
//
|
||||
//
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Omits the specified number of elements before republishing subsequent elements.
|
||||
///
|
||||
/// - Parameter count: The number of elements to omit.
|
||||
/// - Returns: A publisher that does not republish the first `count` elements.
|
||||
public func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self> {
|
||||
return .init(upstream: self, count: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that omits a specified number of elements before republishing
|
||||
/// later elements.
|
||||
public struct Drop<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The number of elements to drop.
|
||||
public let count: Int
|
||||
|
||||
public init(upstream: Upstream, count: Int) {
|
||||
self.upstream = upstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, count: count)
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Drop: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Drop {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
private var count: Int
|
||||
|
||||
fileprivate init(downstream: Downstream, count: Int) {
|
||||
self.downstream = downstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard self.subscription == nil else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
precondition(count >= 0, "count must not be negative")
|
||||
let demandToRequestFromUpstream = pendingDemand + count
|
||||
lock.unlock()
|
||||
if demandToRequestFromUpstream > 0 {
|
||||
subscription.request(demandToRequestFromUpstream)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
// Combine doesn't lock here!
|
||||
if count > 0 {
|
||||
count -= 1
|
||||
return .none
|
||||
}
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
// Combine doesn't lock here!
|
||||
subscription = nil
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard let subscription = self.subscription else {
|
||||
self.pendingDemand += demand
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
// Combine doesn't lock here!
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Drop" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ extension Publishers {
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
@@ -53,8 +53,8 @@ extension Publishers {
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, SubscriberType.Failure == Error
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
@@ -127,6 +127,7 @@ private class _DropWhile<Upstream: Publisher, Downstream: Subscriber>
|
||||
override func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
isCompleted = true
|
||||
// Don't zero out downstream, that's what Combine does (probably a bug)
|
||||
}
|
||||
}
|
||||
@@ -142,11 +143,9 @@ extension Publishers.DropWhile {
|
||||
var description: String { return "DropWhile" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else {
|
||||
assertionFailure("unreachable")
|
||||
return
|
||||
}
|
||||
guard !isCompleted else { return }
|
||||
downstream.receive(completion: completion)
|
||||
isCompleted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +163,7 @@ extension Publishers.TryDropWhile {
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
isCompleted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let encodeSubscriber = _Encode<Upstream, SubscriberType, Coder>(
|
||||
let encodeSubscriber = _Encode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
encoder: encoder
|
||||
)
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// Publishers.Filter.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/3/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes all elements that match a provided closure.
|
||||
///
|
||||
/// - Parameter isIncluded: A closure that takes one element and returns
|
||||
/// a Boolean value indicating whether to republish the element.
|
||||
/// - Returns: A publisher that republishes all elements that satisfy the closure.
|
||||
public func filter(
|
||||
_ isIncluded: @escaping (Output) -> Bool
|
||||
) -> Publishers.Filter<Self> {
|
||||
return Publishers.Filter(upstream: self, isIncluded: isIncluded)
|
||||
}
|
||||
|
||||
/// Republishes all elements that match a provided error-throwing closure.
|
||||
///
|
||||
/// If the `isIncluded` closure throws an error, the publisher fails with that error.
|
||||
///
|
||||
/// - Parameter isIncluded: A closure that takes one element and returns a
|
||||
/// Boolean value indicating whether to republish the element.
|
||||
/// - Returns: A publisher that republishes all elements that satisfy the closure.
|
||||
public func tryFilter(
|
||||
_ isIncluded: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFilter<Self> {
|
||||
return Publishers.TryFilter(upstream: self, isIncluded: isIncluded)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Filter {
|
||||
|
||||
public func filter(
|
||||
_ isIncluded: @escaping (Output) -> Bool
|
||||
) -> Publishers.Filter<Upstream> {
|
||||
return .init(upstream: upstream) { self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
|
||||
public func tryFilter(
|
||||
_ isIncluded: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFilter<Upstream> {
|
||||
return .init(upstream: upstream) { try self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFilter {
|
||||
|
||||
public func filter(
|
||||
_ isIncluded: @escaping (Output) -> Bool
|
||||
) -> Publishers.TryFilter<Upstream> {
|
||||
return .init(upstream: upstream) { try self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
|
||||
public func tryFilter(
|
||||
_ isIncluded: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFilter<Upstream> {
|
||||
return .init(upstream: upstream) { try self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes all elements that match a provided closure.
|
||||
public struct Filter<Upstream: Publisher>: Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that indicates whether to republish an element.
|
||||
public let isIncluded: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, isIncluded: @escaping (Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.isIncluded = isIncluded
|
||||
}
|
||||
|
||||
/// 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.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let filter = Inner(downstream: subscriber, isIncluded: catching(isIncluded))
|
||||
upstream.receive(subscriber: filter)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes all elements that match
|
||||
/// a provided error-throwing closure.
|
||||
public struct TryFilter<Upstream>: Publisher where Upstream: Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A error-throwing closure that indicates whether to republish an element.
|
||||
public let isIncluded: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
isIncluded: @escaping (Upstream.Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.isIncluded = isIncluded
|
||||
}
|
||||
|
||||
/// 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.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
let filter = Inner(downstream: subscriber, isIncluded: catching(isIncluded))
|
||||
upstream.receive(subscriber: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class _Filter<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
|
||||
|
||||
private var _isIncluded: Predicate?
|
||||
|
||||
var isFinished: Bool {
|
||||
return _isIncluded == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, isIncluded: @escaping Predicate) {
|
||||
_isIncluded = isIncluded
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
guard let isIncluded = _isIncluded else { return .none }
|
||||
switch isIncluded(input) {
|
||||
case .success(let isIncluded):
|
||||
return isIncluded ? downstream.receive(input) : .max(1)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard !isFinished else { return }
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_isIncluded = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Filter {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Filter<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure {
|
||||
|
||||
var description: String { return "Filter" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isFinished else { return }
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFilter {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Filter<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error {
|
||||
|
||||
var description: String { return "TryFilter" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isFinished else { return }
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//
|
||||
// Publishers.First.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/8/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes the first element of a stream, then finishes.
|
||||
///
|
||||
/// If this publisher doesn’t receive any elements, it finishes without publishing.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream.
|
||||
public func first() -> Publishers.First<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
|
||||
/// Publishes the first element of a stream to
|
||||
/// satisfy a predicate closure, then finishes.
|
||||
///
|
||||
/// The publisher ignores all elements after the first.
|
||||
/// If this publisher doesn’t receive any elements,
|
||||
/// it finishes without publishing.
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and
|
||||
/// returns a Boolean value that indicates whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream
|
||||
/// that satifies the predicate.
|
||||
public func first(
|
||||
where predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.FirstWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Publishes the first element of a stream to satisfy a
|
||||
/// throwing predicate closure, then finishes.
|
||||
///
|
||||
/// The publisher ignores all elements after the first. If this publisher
|
||||
/// doesn’t receive any elements, it finishes without publishing. If the
|
||||
/// predicate closure throws, the publisher fails with an error.
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and
|
||||
/// returns a Boolean value that indicates whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream
|
||||
/// that satifies the predicate.
|
||||
public func tryFirst(
|
||||
where predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFirstWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes the first element of a stream, then finishes.
|
||||
public struct First<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the first element of a
|
||||
/// stream to satisfy a predicate closure.
|
||||
public struct FirstWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that determines whether to publish an element.
|
||||
public let predicate: (Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the first element of a stream
|
||||
/// to satisfy a throwing predicate closure.
|
||||
public struct TryFirstWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether to publish an element.
|
||||
public let predicate: (Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.First: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.First {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
Void>
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result = newValue
|
||||
return .finished
|
||||
}
|
||||
|
||||
override var description: String { return "First" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FirstWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream, Output, Output, Failure, (Output) -> Bool>
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream, predicate: @escaping (Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if reduce(newValue) {
|
||||
result = newValue
|
||||
return .finished
|
||||
} else {
|
||||
return .continue
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryFirst" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFirstWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Output) throws -> Bool>
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Output
|
||||
) -> PartialCompletion<Void, Error> {
|
||||
do {
|
||||
if try reduce(newValue) {
|
||||
result = newValue
|
||||
return .finished
|
||||
} else {
|
||||
return .continue
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryFirstWhere" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
//
|
||||
// Publishers.FlatMap.swift
|
||||
//
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from an upstream publisher into a new or existing
|
||||
/// publisher.
|
||||
///
|
||||
/// `flatMap` merges the output from all returned publishers into a single stream of
|
||||
/// output.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - maxPublishers: The maximum number of publishers produced by this method.
|
||||
/// - transform: A closure that takes an element as a parameter and returns a
|
||||
/// publisher that produces elements of that type.
|
||||
/// - Returns: A publisher that transforms elements from an upstream publisher into
|
||||
/// a publisher of that element’s type.
|
||||
public func flatMap<Result, Child: Publisher>(
|
||||
maxPublishers: Subscribers.Demand = .unlimited,
|
||||
_ transform: @escaping (Output) -> Child
|
||||
) -> Publishers.FlatMap<Child, Self>
|
||||
where Result == Child.Output, Failure == Child.Failure {
|
||||
return Publishers.FlatMap(upstream: self,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct FlatMap<Child: Publisher, Upstream: Publisher>: Publisher
|
||||
where Child.Failure == Upstream.Failure
|
||||
{
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Child.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let maxPublishers: Subscribers.Demand
|
||||
|
||||
public let transform: (Upstream.Output) -> Child
|
||||
|
||||
public init(upstream: Upstream, maxPublishers: Subscribers.Demand,
|
||||
transform: @escaping (Upstream.Output) -> Child) {
|
||||
self.upstream = upstream
|
||||
self.maxPublishers = maxPublishers
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
/// 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.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap {
|
||||
|
||||
fileprivate final class Inner<Downstream: Subscriber>
|
||||
: CustomStringConvertible,
|
||||
Cancellable
|
||||
where Downstream.Input == Child.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private typealias PendingValue = (
|
||||
value: Downstream.Input,
|
||||
// If the value was buffered at the time it became available, and the child's
|
||||
// demand was left at `.none` we keep track of the child in `pausedChild` so
|
||||
// that we can demand some more of it after sending this value.
|
||||
pausedChild: ChildSubscriber?
|
||||
)
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private let maxPublishers: Subscribers.Demand
|
||||
private let transform: (Input) -> Child
|
||||
|
||||
// Locking rules for this class.
|
||||
// - All mutable state must only be accessed while `lock` is held.
|
||||
// - In order to avoid any deadlock potential, it is absolutely forbidden to have
|
||||
// any sort of call out from this class while the lock is held. This is why
|
||||
// the draining of the work queue uses a relatively complex pattern.
|
||||
private var downstream: Downstream?
|
||||
private var childSubscribers = Set<ChildSubscriber>()
|
||||
private var downstreamDemand = Subscribers.Demand.unlimited
|
||||
private var valuesToSend = [PendingValue]()
|
||||
private var queueIsBeingProcessed = false
|
||||
private var sendFinishedAfterDrainingQueue = false
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
var description: String { return "FlatMap" }
|
||||
|
||||
init(downstream: Downstream,
|
||||
maxPublishers: Subscribers.Demand,
|
||||
transform: @escaping (Upstream.Output) -> Child) {
|
||||
self.downstream = downstream
|
||||
self.maxPublishers = maxPublishers
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
final func cancel() {
|
||||
|
||||
let (upstreamToCancel, childrenToCancel) = lock
|
||||
.do { () -> (Subscription?, Set<ChildSubscriber>) in
|
||||
let upstreamToCancel = upstreamSubscription
|
||||
upstreamSubscription = nil
|
||||
return (upstreamToCancel, lockedDeactivateAndReturnChildrenToCancel())
|
||||
}
|
||||
|
||||
upstreamToCancel?.cancel()
|
||||
cancelChildren(childrenToCancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private implementation
|
||||
extension Publishers.FlatMap.Inner {
|
||||
|
||||
private func deactivate() {
|
||||
cancelChildren(lock.do(lockedDeactivateAndReturnChildrenToCancel))
|
||||
}
|
||||
|
||||
// Must be called with lock held.
|
||||
private func lockedDeactivateAndReturnChildrenToCancel() -> Set<ChildSubscriber> {
|
||||
downstream = nil
|
||||
downstreamDemand = .none
|
||||
let result = childSubscribers
|
||||
childSubscribers.removeAll()
|
||||
upstreamSubscription = nil
|
||||
return result
|
||||
}
|
||||
|
||||
private func cancelChildren(_ childrenToCancel: Set<ChildSubscriber>) {
|
||||
childrenToCancel.forEach { $0.cancel() }
|
||||
}
|
||||
|
||||
/// In a thread-safe way, this function performs the passed in work with the lock held
|
||||
/// and then checks to see if either upstream or any of the child subscriptions remain
|
||||
/// active. If there are no remaining active subscriptions, it enqueues the sending
|
||||
/// of `.finished` downstream using the processing queue.
|
||||
/// - Parameter lockedWork: block to be formed with the lock held.
|
||||
private final func maybeSendFinishedAfterExecutingWork(lockedWork: () -> Void) {
|
||||
let shouldProcessQueue: Bool = lock.do {
|
||||
lockedWork()
|
||||
if childSubscribers.isEmpty && upstreamSubscription == nil {
|
||||
sendFinishedAfterDrainingQueue = true
|
||||
if !queueIsBeingProcessed {
|
||||
queueIsBeingProcessed = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if shouldProcessQueue {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private func receivedCompletion(_ completion: Subscribers.Completion<Failure>,
|
||||
fromChild child: ChildSubscriber) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
removeActiveSubscription(forChild: child)
|
||||
case .failure:
|
||||
downstream?.receive(completion: completion)
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
private func removeActiveSubscription(forChild child: ChildSubscriber) {
|
||||
maybeSendFinishedAfterExecutingWork { childSubscribers.remove(child) }
|
||||
}
|
||||
|
||||
private func receivedValue(_ value: Child.Output,
|
||||
fromChild child: ChildSubscriber) -> Subscribers.Demand {
|
||||
// When receiving a value from a child, we need to determine what additional
|
||||
// demand to return to the child. Apple's logic for this determination is as
|
||||
// follows:
|
||||
// - If we are in `.unlimited` mode, we always request `.none` additional
|
||||
// else
|
||||
// - If there is a surplus relative to the demand, we request `.none`
|
||||
// else
|
||||
// - There is not yet a surplus, so request `.max(1)` more from the child
|
||||
|
||||
let (surplusAvailable, processTheQueue): (Bool, Bool) = lock.do {
|
||||
// If we already have enough values to satisfy the demand, we "buffer" this
|
||||
// child value establishing a surplus.
|
||||
if downstreamDemand <= valuesToSend.count {
|
||||
valuesToSend.append((value, child))
|
||||
return (surplusAvailable: true, processTheQueue: false)
|
||||
} else {
|
||||
valuesToSend.append((value, nil))
|
||||
if queueIsBeingProcessed {
|
||||
return (surplusAvailable: false, processTheQueue: false)
|
||||
}
|
||||
queueIsBeingProcessed = true
|
||||
return (surplusAvailable: false, processTheQueue: true)
|
||||
}
|
||||
}
|
||||
|
||||
let demandResult = surplusAvailable || demandForChild() == .unlimited
|
||||
? Subscribers.Demand.none
|
||||
: .max(1)
|
||||
|
||||
if processTheQueue {
|
||||
processQueue()
|
||||
}
|
||||
|
||||
return demandResult
|
||||
}
|
||||
|
||||
private func demandForChild() -> Subscribers.Demand {
|
||||
return downstreamDemand == .unlimited ? .unlimited : .max(1)
|
||||
}
|
||||
|
||||
private enum QueueWorkStatus {
|
||||
case noWork
|
||||
case sendFinish
|
||||
case sendValues(values: ArraySlice<PendingValue>)
|
||||
}
|
||||
|
||||
private func processQueue() {
|
||||
assert(queueIsBeingProcessed)
|
||||
|
||||
// We loop processing the queue in case somebody put stuff on the queue while we
|
||||
// were sending values with the lock unlocked.
|
||||
while true {
|
||||
let work: QueueWorkStatus = lock.do {
|
||||
if downstreamDemand == .none || valuesToSend.isEmpty {
|
||||
if sendFinishedAfterDrainingQueue && valuesToSend.isEmpty {
|
||||
return .sendFinish
|
||||
} else {
|
||||
queueIsBeingProcessed = false
|
||||
return .noWork
|
||||
}
|
||||
}
|
||||
|
||||
let countToSend = min(valuesToSend.count, downstreamDemand.max ?? .max)
|
||||
let result = valuesToSend[0..<countToSend]
|
||||
// TODO: Consider an alternative storage to avoid O(n) removeFirst
|
||||
valuesToSend.removeFirst(countToSend)
|
||||
downstreamDemand -= countToSend
|
||||
return .sendValues(values: result)
|
||||
}
|
||||
|
||||
guard let downstream = downstream else { return }
|
||||
|
||||
switch work {
|
||||
case .noWork:
|
||||
return
|
||||
case .sendFinish:
|
||||
downstream.receive(completion: .finished)
|
||||
deactivate()
|
||||
return
|
||||
case .sendValues(let values):
|
||||
var newDemand = Subscribers.Demand.none
|
||||
values.forEach {
|
||||
newDemand += downstream.receive($0.value)
|
||||
// pausedChild is present only if the value was buffered and the
|
||||
// child's demand was left at `.none`. In that case, once we send the
|
||||
// buffered value, we need to tell the child to get another value.
|
||||
$0.pausedChild?.request(.max(1))
|
||||
}
|
||||
|
||||
if newDemand != .none {
|
||||
lock.do { downstreamDemand += newDemand }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This `Subscriber` implementation is for `FlatMap`'s upstream subscription
|
||||
extension Publishers.FlatMap.Inner: Subscriber {
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream?.receive(subscription: self)
|
||||
subscription.request(maxPublishers)
|
||||
}
|
||||
|
||||
/// Receive a new value from the upstream subscription. A new child subscription
|
||||
/// will be made on the `Child` that the input value is transformed into.
|
||||
/// - Parameter input: a value to be transformed by `transform`
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let newChildSubscriber = ChildSubscriber(parent: self)
|
||||
|
||||
lock.do { _ = childSubscribers.insert(newChildSubscriber) }
|
||||
|
||||
self.transform(input).subscribe(newChildSubscriber)
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
maybeSendFinishedAfterExecutingWork { upstreamSubscription = nil }
|
||||
case .failure:
|
||||
downstream?.receive(completion: completion)
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inner is the `Subscription` for `Downstream`
|
||||
extension Publishers.FlatMap.Inner: Subscription {
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
let (drainTheQueue, becameUnlimited) = lock.do { () -> (Bool, Bool) in
|
||||
let becameUnlimited = demand == .unlimited && downstreamDemand != .unlimited
|
||||
downstreamDemand = demand
|
||||
defer { queueIsBeingProcessed = true }
|
||||
return (!queueIsBeingProcessed, becameUnlimited)
|
||||
}
|
||||
|
||||
if becameUnlimited {
|
||||
// TODO: This code isn't yet thread safe. The correct change is to do this
|
||||
// through the queue just like sending values and finished. Finished is
|
||||
// done through the queue as a bit of a hack. The right design is to have
|
||||
// an enum of actions on the queue. That enum will include (send value,
|
||||
// send finished, set child demand).
|
||||
let newChildDemand = demandForChild()
|
||||
childSubscribers.forEach { $0.request(newChildDemand) }
|
||||
}
|
||||
|
||||
if drainTheQueue {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner {
|
||||
/// ChildSubscriber is needed to help implement the backpressure/demand strategy.
|
||||
/// Specifically, a custom subscriber is needed to manage the demand of the child
|
||||
/// subscription:
|
||||
/// - Send .max(1) request when the subscription is received
|
||||
/// - Send .max(1) request when downstream subscriber demands more and a previously
|
||||
/// buffered value from the child was sent. (When the value was buffered, the
|
||||
/// child's demand reached .none - effectively pausing the child.)
|
||||
fileprivate final class ChildSubscriber: Hashable {
|
||||
internal typealias Input = Downstream.Input
|
||||
internal typealias Failure = Downstream.Failure
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private unowned let _parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>
|
||||
|
||||
init(parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>) {
|
||||
_parent = parent
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
_upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
public static func == (lhs: ChildSubscriber, rhs: ChildSubscriber) -> Bool {
|
||||
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Cancellable {
|
||||
fileprivate func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Subscriber {
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(_parent.demandForChild())
|
||||
} else {
|
||||
assertionFailure()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return _parent.receivedValue(input, fromChild: self)
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
_parent.receivedCompletion(completion, fromChild: self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Publishers.IgnoreOutput.swift
|
||||
//
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ingores all upstream elements, but passes along a completion
|
||||
/// state (finished or failed).
|
||||
///
|
||||
/// The output type of this publisher is `Never`.
|
||||
/// - Returns: A publisher that ignores all upstream elements.
|
||||
public func ignoreOutput() -> Publishers.IgnoreOutput<Self> {
|
||||
return Publishers.IgnoreOutput(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that ignores all upstream elements, but passes along a completion
|
||||
/// state (finish or failed).
|
||||
public struct IgnoreOutput<Upstream: Publisher>: Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Never
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
/// 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.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Upstream.Failure, Downstream.Input == Never {
|
||||
let inner = Inner<Downstream>(downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.IgnoreOutput {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Downstream.Input == Never,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Output = Never
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "IgnoreOutput" }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
// ignore and requests from downstream since we'll never send
|
||||
// any values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.IgnoreOutput: Equatable where Upstream: Equatable {}
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// Publishers.Last.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/9/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Only publishes the last element of a stream, after the stream finishes.
|
||||
/// - Returns: A publisher that only publishes the last element of a stream.
|
||||
public func last() -> Publishers.Last<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
|
||||
/// Only publishes the last element of a stream that satisfies a predicate closure,
|
||||
/// after the stream finishes.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the last element satisfying
|
||||
/// the given predicate.
|
||||
public func last(
|
||||
where predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.LastWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Only publishes the last element of a stream that satisfies an error-throwing
|
||||
/// predicate closure, after the stream finishes.
|
||||
///
|
||||
/// If the predicate closure throws, the publisher fails with the thrown error.
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the last element satisfying
|
||||
/// the given predicate.
|
||||
public func tryLast(
|
||||
where predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryLastWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that only publishes the last element of a stream,
|
||||
/// after the stream finishes.
|
||||
public struct Last<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the last element of a stream that satisfies
|
||||
/// a predicate closure, once the stream finishes.
|
||||
public struct LastWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that determines whether to publish an element.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the last element of a stream that satisfies
|
||||
/// an error-throwing predicate closure, once the stream finishes.
|
||||
public struct TryLastWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether to publish an element.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Error, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Last: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Last {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
Void>
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result = newValue
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Last" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.LastWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if reduce(newValue) {
|
||||
result = newValue
|
||||
}
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "LastWhere" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryLastWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if try reduce(newValue) {
|
||||
result = newValue
|
||||
}
|
||||
return .continue
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryLastWhere" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Publishers.MakeConnectable.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher where Failure == Never {
|
||||
|
||||
/// Creates a connectable wrapper around the publisher.
|
||||
///
|
||||
/// - Returns: A `ConnectablePublisher` wrapping this publisher.
|
||||
public func makeConnectable() -> Publishers.MakeConnectable<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct MakeConnectable<Upstream: Publisher>: ConnectablePublisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private let inner: Multicast<Upstream, PassthroughSubject<Output, Failure>>
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
inner = upstream.multicast(subject: .init())
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
inner.subscribe(subscriber)
|
||||
}
|
||||
|
||||
public func connect() -> Cancellable {
|
||||
return inner.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Anton Nazarov on 25.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from the upstream publisher with a provided closure.
|
||||
@@ -39,7 +41,7 @@ extension Publisher {
|
||||
extension Publishers {
|
||||
/// A publisher that transforms all elements from the upstream publisher with
|
||||
/// a provided closure.
|
||||
public struct Map<Upstream: Publisher, Output> : Publisher {
|
||||
public struct Map<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
@@ -54,6 +56,12 @@ extension Publishers {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that transforms all elements from the upstream publisher
|
||||
@@ -78,12 +86,6 @@ extension Publishers {
|
||||
}
|
||||
|
||||
extension Publishers.Map {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
|
||||
public func map<Result>(
|
||||
_ transform: @escaping (Output) -> Result
|
||||
@@ -103,8 +105,7 @@ extension Publishers.TryMap {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
|
||||
public func map<Result>(
|
||||
@@ -120,89 +121,158 @@ extension Publishers.TryMap {
|
||||
}
|
||||
}
|
||||
|
||||
private class _Map<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Transform = (Input) -> Result<Downstream.Input, Downstream.Failure>
|
||||
|
||||
fileprivate var _transform: Transform?
|
||||
|
||||
var isCompleted: Bool {
|
||||
return _transform == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, transform: @escaping Transform) {
|
||||
_transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
switch _transform?(input) {
|
||||
case .success(let output)?:
|
||||
return downstream.receive(output)
|
||||
case .failure(let error)?:
|
||||
downstream.receive(completion: .failure(error))
|
||||
_transform = nil
|
||||
return .none
|
||||
case nil:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Map {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Map<Upstream, Downstream>,
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
where Downstream.Failure == Upstream.Failure
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let map: (Input) -> Output
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream, map: @escaping (Input) -> Output) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(map(input))
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Map" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryMap {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Map<Upstream, Downstream>,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
where Downstream.Failure == Error
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let map: (Input) throws -> Output
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream,
|
||||
map: @escaping (Input) throws -> Output) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
if !isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
do {
|
||||
return try downstream.receive(map(input))
|
||||
} catch {
|
||||
lock.lock()
|
||||
let subscription: Subscription?
|
||||
switch status {
|
||||
case let .subscribed(upstreamSubscription):
|
||||
subscription = upstreamSubscription
|
||||
case .awaitingSubscription, .terminal:
|
||||
subscription = nil
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_transform = nil
|
||||
super.cancel()
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "TryMap" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +32,11 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure,
|
||||
Upstream.Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let mapErrorSubscriber = _MapError<Upstream, SubscriberType>(
|
||||
downstream: subscriber,
|
||||
transform: transform
|
||||
)
|
||||
upstream.subscribe(mapErrorSubscriber)
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,41 +60,50 @@ extension Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
private final class _MapError<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Output = Downstream.Input
|
||||
extension Publishers.MapError {
|
||||
|
||||
private let _transform: (Upstream.Failure) -> Downstream.Failure
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "MapError" }
|
||||
private let downstream: Downstream
|
||||
private let map: (Upstream.Failure) -> Downstream.Failure
|
||||
|
||||
init(downstream: Downstream,
|
||||
transform: @escaping (Upstream.Failure) -> Downstream.Failure) {
|
||||
self._transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
var description: String { return "MapError" }
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(_transform(error)))
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
init(downstream: Downstream,
|
||||
map: @escaping (Upstream.Failure) -> Downstream.Failure) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(map(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
${template_header}
|
||||
//
|
||||
// Publishers.MapKeyPath.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
%{
|
||||
from gyb_opencombine_support import (
|
||||
suffix_variadic,
|
||||
list_with_suffix_variadic
|
||||
)
|
||||
|
||||
instantiations = [(1, '', ''),
|
||||
(2, 'two', 'second '),
|
||||
(3, 'three', 'third ')]
|
||||
|
||||
def key_path_var(index, arity):
|
||||
return suffix_variadic('keyPath', index, arity)
|
||||
|
||||
def make_publisher_name(arity):
|
||||
return suffix_variadic('MapKeyPath', arity, arity)
|
||||
|
||||
def make_output_types(arity):
|
||||
return list_with_suffix_variadic('Output', arity)
|
||||
}%
|
||||
extension Publisher {
|
||||
% for arity, cardinal, _ in instantiations:
|
||||
% result_types = list_with_suffix_variadic('Result', arity)
|
||||
% cs_result_types = ', '.join(result_types)
|
||||
%
|
||||
% method_args = \
|
||||
% ['_ {}: KeyPath<Output, {}>'.format(key_path_var(i, arity), result_types[i]) \
|
||||
% for i in range(arity)]
|
||||
% method_args_joined = ',\n '.join(method_args)
|
||||
%
|
||||
% init_args = ['{}: {}'.format(key_path_var(i, arity), key_path_var(i, arity)) \
|
||||
% for i in range(arity)]
|
||||
% init_args_joined = ',\n '.join(init_args)
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
%
|
||||
% doc_cardinal = 'a keyt path' if arity == 1 else cardinal + ' key paths'
|
||||
|
||||
/// Returns a publisher that publishes the values of ${doc_cardinal} as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
% for i in range(arity):
|
||||
% ordinal = 'another ' if i == 1 else 'a ' + instantiations[i][2]
|
||||
/// - ${key_path_var(i, arity)}: The key path of ${ordinal}property on `Output`
|
||||
% end
|
||||
%
|
||||
% doc_comment_suffix = 'value of the key path' \
|
||||
% if arity == 1 else 'values of {} key paths as a tuple'.format(cardinal)
|
||||
/// - Returns: A publisher that publishes the ${doc_comment_suffix}.
|
||||
public func map<${cs_result_types}>(
|
||||
${method_args_joined}
|
||||
) -> Publishers.${publisher_name}<Self, ${cs_result_types}> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
${init_args_joined}
|
||||
)
|
||||
}
|
||||
% end
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
% for arity, cardinal, ordinal in instantiations:
|
||||
%
|
||||
% doc_comment_suffix = 'value of a key path' \
|
||||
% if arity == 1 else 'values of {} key paths as a tuple'.format(cardinal)
|
||||
%
|
||||
% output_types = make_output_types(arity)
|
||||
% cs_output_types = ', '.join(output_types)
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
|
||||
/// A publisher that publishes the ${doc_comment_suffix}.
|
||||
public struct ${publisher_name}<Upstream: Publisher, ${cs_output_types}>: Publisher {
|
||||
|
||||
% if arity != 1:
|
||||
public typealias Output = (${cs_output_types})
|
||||
|
||||
% end
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
% for i in range(arity):
|
||||
% ordinal = instantiations[i][2]
|
||||
/// The key path of a ${ordinal}property to publish.
|
||||
public let ${key_path_var(i, arity)}: KeyPath<Upstream.Output, ${output_types[i]}>
|
||||
|
||||
% end
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
% end
|
||||
}
|
||||
% for arity, _, _ in instantiations:
|
||||
% output_types = make_output_types(arity)
|
||||
% cs_output_types = ', '.join(output_types)
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
|
||||
extension Publishers.${publisher_name} {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
% for i in range(arity):
|
||||
private let ${key_path_var(i, arity)}: KeyPath<Input, ${output_types[i]}>
|
||||
|
||||
% end
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.${publisher_name}<Upstream, ${cs_output_types}>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
% for i in range(arity):
|
||||
self.${key_path_var(i, arity)} = parent.${key_path_var(i, arity)}
|
||||
% end
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
% output_components = \
|
||||
% ['input[keyPath: {}]'.format(key_path_var(i, arity)) for i in range(arity)]
|
||||
% output_components_joined = ',\n '.join(output_components)
|
||||
let output = (
|
||||
${output_components_joined}
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
% inner_description = 'ValueForKey' + ('' if arity == 1 else 's')
|
||||
var description: String { return "${inner_description}" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
% for i in range(arity):
|
||||
("${key_path_var(i, arity)}", ${key_path_var(i, arity)}),
|
||||
% end
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
% end
|
||||
@@ -5,8 +5,19 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Applies a closure to create a subject that delivers elements to subscribers.
|
||||
///
|
||||
/// Use a multicast publisher when you have multiple downstream subscribers, but you
|
||||
/// want upstream publishers to only process one `receive(_:)` call per event.
|
||||
/// In contrast with `multicast(subject:)`, this method produces a publisher that
|
||||
/// creates a separate Subject for each subscriber.
|
||||
///
|
||||
/// - Parameter createSubject: A closure to create a new Subject each time
|
||||
/// a subscriber attaches to the multicast publisher.
|
||||
public func multicast<SubjectType: Subject>(
|
||||
_ createSubject: @escaping () -> SubjectType
|
||||
) -> Publishers.Multicast<Self, SubjectType>
|
||||
@@ -15,6 +26,14 @@ extension Publisher {
|
||||
return Publishers.Multicast(upstream: self, createSubject: createSubject)
|
||||
}
|
||||
|
||||
/// Provides a subject to deliver elements to multiple subscribers.
|
||||
///
|
||||
/// Use a multicast publisher when you have multiple downstream subscribers, but you
|
||||
/// want upstream publishers to only process one `receive(_:)` call per event.
|
||||
/// In contrast with `multicast(_:)`, this method produces a publisher shares
|
||||
/// the provided Subject among all the downstream subscribers.
|
||||
///
|
||||
/// - Parameter subject: A subject to deliver elements to downstream subscribers.
|
||||
public func multicast<SubjectType: Subject>(
|
||||
subject: SubjectType
|
||||
) -> Publishers.Multicast<Self, SubjectType>
|
||||
@@ -26,43 +45,67 @@ extension Publisher {
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that uses a subject to deliver elements to multiple subscribers.
|
||||
public final class Multicast<Upstream: Publisher, SubjectType: Subject>
|
||||
: ConnectablePublisher
|
||||
where Upstream.Failure == SubjectType.Failure,
|
||||
Upstream.Output == SubjectType.Output
|
||||
where Upstream.Failure == SubjectType.Failure,
|
||||
Upstream.Output == SubjectType.Output
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure to create a new Subject each time a subscriber attaches
|
||||
/// to the multicast publisher.
|
||||
public let createSubject: () -> SubjectType
|
||||
|
||||
private lazy var _subject: SubjectType = self.createSubject()
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var subject: SubjectType?
|
||||
|
||||
private var lazySubject: SubjectType {
|
||||
lock.lock()
|
||||
if let subject = subject {
|
||||
lock.unlock()
|
||||
return subject
|
||||
}
|
||||
|
||||
let subject = createSubject()
|
||||
self.subject = subject
|
||||
lock.unlock()
|
||||
return subject
|
||||
}
|
||||
|
||||
/// Creates a multicast publisher that applies a closure to create a subject
|
||||
/// that delivers elements to subscribers.
|
||||
///
|
||||
/// - Parameter upstream: The publisher from which this publisher receives
|
||||
/// elements.
|
||||
/// - Parameter createSubject: A closure to create a new Subject each time
|
||||
/// a subscriber attaches to the multicast publisher.
|
||||
public init(upstream: Upstream, createSubject: @escaping () -> SubjectType) {
|
||||
self.upstream = upstream
|
||||
self.createSubject = createSubject
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where SubjectType.Failure == Downstream.Failure,
|
||||
SubjectType.Output == Downstream.Input
|
||||
{
|
||||
_subject.subscribe(Inner(downstream: subscriber))
|
||||
lazySubject.subscribe(Inner(parent: self, downstream: subscriber))
|
||||
}
|
||||
|
||||
public func connect() -> Cancellable {
|
||||
|
||||
let subscriber = SubjectSubscriber(_subject)
|
||||
|
||||
upstream.subscribe(subscriber)
|
||||
|
||||
return AnyCancellable {
|
||||
subscriber.downstreamSubject = nil
|
||||
}
|
||||
return upstream.subscribe(lazySubject)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,32 +113,105 @@ extension Publishers {
|
||||
extension Publishers.Multicast {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "Multicast" }
|
||||
private enum State {
|
||||
case ready(upstream: Upstream, downstream: Downstream)
|
||||
case subscribed(upstream: Upstream,
|
||||
downstream: Downstream,
|
||||
subjectSubscription: Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
fileprivate init(parent: Publishers.Multicast<Upstream, SubjectType>,
|
||||
downstream: Downstream) {
|
||||
state = .ready(upstream: parent.upstream, downstream: downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "Multicast" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
lock.lock()
|
||||
guard case let .ready(upstream, downstream) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .subscribed(upstream: upstream,
|
||||
downstream: downstream,
|
||||
subjectSubscription: subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(input)
|
||||
if newDemand > 0 {
|
||||
subjectSubscription.request(newDemand)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subjectSubscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subjectSubscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that prints log messages for all publishing events, optionally
|
||||
@@ -43,8 +45,8 @@ extension Publishers {
|
||||
self.stream = stream
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, prefix: prefix, stream: stream)
|
||||
upstream.subscribe(inner)
|
||||
@@ -65,99 +67,97 @@ extension Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<Downstream: Subscriber>: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
typealias Input = Downstream.Input
|
||||
typealias Failure = Downstream.Failure
|
||||
extension Publishers.Print {
|
||||
private final class Inner<Downstream: Subscriber>: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
typealias Input = Downstream.Input
|
||||
typealias Failure = Downstream.Failure
|
||||
|
||||
private var _downstream: Downstream
|
||||
private let _prefix: String
|
||||
private var _stream: TextOutputStream
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private let _printerLock = Lock(recursive: false)
|
||||
/// A concrete type wrapper around an abstract stream.
|
||||
private struct PrintTarget: TextOutputStream {
|
||||
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
_downstream = downstream
|
||||
_prefix = prefix
|
||||
_stream = stream ?? StdoutStream()
|
||||
}
|
||||
var stream: TextOutputStream
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
_log("receive subscription", value: subscription)
|
||||
_upstreamSubscription = subscription
|
||||
_downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
_log("receive value", value: input)
|
||||
let demand = _downstream.receive(input)
|
||||
_logDemand(demand, synchronous: true)
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
_log("receive finished")
|
||||
case .failure(let error):
|
||||
_log("receive error", value: error)
|
||||
mutating func write(_ string: String) {
|
||||
stream.write(string)
|
||||
}
|
||||
}
|
||||
_downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
_logDemand(demand, synchronous: false)
|
||||
_upstreamSubscription?.request(demand)
|
||||
}
|
||||
private var downstream: Downstream
|
||||
private let prefix: String
|
||||
private var stream: PrintTarget?
|
||||
private var subscription: Subscription?
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
func cancel() {
|
||||
_log("receive cancel")
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Print" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
private func _log(_ description: String,
|
||||
value: Any? = nil,
|
||||
additionalInfo: String = "") {
|
||||
_printerLock.do {
|
||||
if !_prefix.isEmpty {
|
||||
_stream.write(_prefix)
|
||||
_stream.write(": ")
|
||||
}
|
||||
_stream.write(description)
|
||||
if let value = value {
|
||||
_stream.write(": (")
|
||||
_stream.write(String(describing: value))
|
||||
_stream.write(")")
|
||||
}
|
||||
if !additionalInfo.isEmpty {
|
||||
_stream.write(" (")
|
||||
_stream.write(additionalInfo)
|
||||
_stream.write(")")
|
||||
}
|
||||
_stream.write("\n")
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
self.downstream = downstream
|
||||
self.prefix = prefix.isEmpty ? "" : "\(prefix): "
|
||||
self.stream = stream.map(PrintTarget.init)
|
||||
}
|
||||
}
|
||||
|
||||
private func _logDemand(_ demand: Subscribers.Demand, synchronous: Bool) {
|
||||
let synchronouslyStr = synchronous ? "synchronous" : ""
|
||||
if let max = demand.max {
|
||||
_log("request max", value: max, additionalInfo: synchronouslyStr)
|
||||
} else {
|
||||
_log("request unlimited", additionalInfo: synchronouslyStr)
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
log("\(prefix)receive subscription: (\(subscription))")
|
||||
lock.do {
|
||||
self.subscription = subscription
|
||||
}
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
log("\(prefix)receive value: (\(input))")
|
||||
let demand = downstream.receive(input)
|
||||
|
||||
if let max = demand.max {
|
||||
log("\(prefix)request max: (\(max)) (synchronous)")
|
||||
} else {
|
||||
log("\(prefix)request unlimited (synchronous)")
|
||||
}
|
||||
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
log("\(prefix)receive finished")
|
||||
case .failure(let error):
|
||||
log("\(prefix)receive error: (\(error))")
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let max = demand.max {
|
||||
log("\(prefix)request max: (\(max))")
|
||||
} else {
|
||||
log("\(prefix)request unlimited")
|
||||
}
|
||||
subscription?.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
log("\(prefix)receive cancel")
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Print" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
private func log(_ text: String) {
|
||||
if var stream = stream {
|
||||
Swift.print(text, to: &stream)
|
||||
} else {
|
||||
Swift.print(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StdoutStream: TextOutputStream {
|
||||
mutating func write(_ string: String) {
|
||||
print(string, terminator: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// Publishers.Reduce.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Applies a closure that accumulates each element of a stream and publishes
|
||||
/// a final result upon completion.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The value the closure receives the first time it is called.
|
||||
/// - nextPartialResult: A closure that takes the previously-accumulated value and
|
||||
/// the next element from the upstream publisher to produce a new value.
|
||||
/// - Returns: A publisher that applies the closure to all received elements and
|
||||
/// produces an accumulated value when the upstream publisher finishes.
|
||||
public func reduce<Accumulator>(
|
||||
_ initialResult: Accumulator,
|
||||
_ nextPartialResult: @escaping (Accumulator, Output) -> Accumulator
|
||||
) -> Publishers.Reduce<Self, Accumulator> {
|
||||
return .init(upstream: self,
|
||||
initial: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
|
||||
/// Applies an error-throwing closure that accumulates each element of a stream and
|
||||
/// publishes a final result upon completion.
|
||||
///
|
||||
/// If the closure throws an error, the publisher fails, passing the error
|
||||
/// to its subscriber.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The value the closure receives the first time it is called.
|
||||
/// - nextPartialResult: An error-throwing closure that takes
|
||||
/// the previously-accumulated value and the next element from the upstream
|
||||
/// publisher to produce a new value.
|
||||
/// - Returns: A publisher that applies the closure to all received elements and
|
||||
/// produces an accumulated value when the upstream publisher finishes.
|
||||
public func tryReduce<Accumulator>(
|
||||
_ initialResult: Accumulator,
|
||||
_ nextPartialResult: @escaping (Accumulator, Output) throws -> Accumulator
|
||||
) -> Publishers.TryReduce<Self, Accumulator> {
|
||||
return .init(upstream: self,
|
||||
initial: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that applies a closure to all received elements and produces
|
||||
/// an accumulated value when the upstream publisher finishes.
|
||||
public struct Reduce<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The initial value provided on the first invocation of the closure.
|
||||
public let initial: Output
|
||||
|
||||
/// A closure that takes the previously-accumulated value and the next element
|
||||
/// from the upstream publisher to produce a new value.
|
||||
public let nextPartialResult: (Output, Upstream.Output) -> Output
|
||||
|
||||
public init(upstream: Upstream,
|
||||
initial: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) -> Output) {
|
||||
self.upstream = upstream
|
||||
self.initial = initial
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
initial: initial,
|
||||
reduce: nextPartialResult)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that applies an error-throwing closure to all received elements and
|
||||
/// produces an accumulated value when the upstream publisher finishes.
|
||||
public struct TryReduce<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The initial value provided on the first invocation of the closure.
|
||||
public let initial: Output
|
||||
|
||||
/// An error-throwing closure that takes the previously-accumulated value and
|
||||
/// the next element from the upstream to produce a new value.
|
||||
///
|
||||
/// If this closure throws an error, the publisher fails and passes the error
|
||||
/// to its subscriber.
|
||||
public let nextPartialResult: (Output, Upstream.Output) throws -> Output
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
initial: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) throws -> Output
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.initial = initial
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
initial: initial,
|
||||
reduce: nextPartialResult)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Reduce {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Output, Upstream.Output) -> Output>
|
||||
where Downstream.Input == Output, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result = reduce(result!, newValue)
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Reduce" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryReduce {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Output, Upstream.Output) throws -> Output>
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
result = try reduce(result!, newValue)
|
||||
return .continue
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryReduce" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// Publishers.ReplaceError.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher fails with an error, this publisher emits the provided
|
||||
/// element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher fails.
|
||||
/// - Returns: A publisher that replaces an error from the upstream publisher with
|
||||
/// the provided output element.
|
||||
public func replaceError(with output: Output) -> Publishers.ReplaceError<Self> {
|
||||
return .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that replaces any errors in the stream with a provided element.
|
||||
public struct ReplaceError<Upstream: Publisher>: Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The element with which to replace errors from the upstream publisher.
|
||||
public let output: Upstream.Output
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream,
|
||||
output: Output) {
|
||||
self.upstream = upstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
/// 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.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceError: Equatable
|
||||
where Upstream: Equatable, Upstream.Output: Equatable
|
||||
{}
|
||||
|
||||
extension Publishers.ReplaceError {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let output: Upstream.Output
|
||||
private let downstream: Downstream
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private var terminated = false
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Upstream.Output) {
|
||||
self.downstream = downstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
pendingDemand -= 1
|
||||
lock.unlock()
|
||||
let demand = downstream.receive(input)
|
||||
guard demand > 0 else {
|
||||
return .none
|
||||
}
|
||||
lock.lock()
|
||||
pendingDemand += demand
|
||||
lock.unlock()
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure:
|
||||
lock.lock()
|
||||
// If there was no demand from downstream,
|
||||
// ReplaceError does not forward the value that
|
||||
// replaces the error until it is requested.
|
||||
guard pendingDemand > 0 else {
|
||||
terminated = true
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
if terminated {
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReplaceError" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
//
|
||||
// Publishers.Scan.swift
|
||||
//
|
||||
// Created by Eric Patey on 26.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms elements from the upstream publisher by providing the current element
|
||||
/// to a closure along with the last value returned by the closure.
|
||||
///
|
||||
/// let pub = (0...5)
|
||||
/// .publisher
|
||||
/// .scan(0, { return $0 + $1 })
|
||||
/// .sink(receiveValue: { print ("\($0)", terminator: " ") })
|
||||
/// // Prints "0 1 3 6 10 15 ".
|
||||
///
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The previous result returned by the `nextPartialResult`
|
||||
/// closure.
|
||||
/// - nextPartialResult: A closure that takes as its arguments the previous value
|
||||
/// returned by the closure and the next element emitted from the upstream
|
||||
/// publisher.
|
||||
/// - Returns: A publisher that transforms elements by applying a closure that
|
||||
/// receives its previous return value and the next element from the upstream
|
||||
/// publisher.
|
||||
public func scan<Result>(
|
||||
_ initialResult: Result,
|
||||
_ nextPartialResult: @escaping (Result, Output) -> Result
|
||||
) -> Publishers.Scan<Self, Result> {
|
||||
return .init(upstream: self,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
|
||||
/// Transforms elements from the upstream publisher by providing the current element
|
||||
/// to an error-throwing closure along with the last value returned by the closure.
|
||||
///
|
||||
/// If the closure throws an error, the publisher fails with the error.
|
||||
/// - Parameters:
|
||||
/// - initialResult: The previous result returned by the `nextPartialResult`
|
||||
/// closure.
|
||||
/// - nextPartialResult: An error-throwing closure that takes as its arguments the
|
||||
/// previous value returned by the closure and the next element emitted from the
|
||||
/// upstream publisher.
|
||||
/// - Returns: A publisher that transforms elements by applying a closure that
|
||||
/// receives its previous return value and the next element from the upstream
|
||||
/// publisher.
|
||||
public func tryScan<Result>(
|
||||
_ initialResult: Result,
|
||||
_ nextPartialResult: @escaping (Result, Output) throws -> Result
|
||||
) -> Publishers.TryScan<Self, Result> {
|
||||
return .init(upstream: self,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct Scan<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let initialResult: Output
|
||||
|
||||
public let nextPartialResult: (Output, Upstream.Output) -> Output
|
||||
|
||||
public init(upstream: Upstream,
|
||||
initialResult: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) -> Output) {
|
||||
self.upstream = upstream
|
||||
self.initialResult = initialResult
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult))
|
||||
}
|
||||
}
|
||||
|
||||
public struct TryScan<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let initialResult: Output
|
||||
|
||||
public let nextPartialResult: (Output, Upstream.Output) throws -> Output
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
initialResult: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) throws -> Output
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.initialResult = initialResult
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Scan {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let nextPartialResult: (Downstream.Input, Input) -> Downstream.Input
|
||||
|
||||
private var result: Downstream.Input
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
initialResult: Downstream.Input,
|
||||
nextPartialResult: @escaping (Downstream.Input, Input) -> Downstream.Input
|
||||
)
|
||||
{
|
||||
self.downstream = downstream
|
||||
self.result = initialResult
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
result = nextPartialResult(result, input)
|
||||
return downstream.receive(result)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Scan" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("result", result)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryScan {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let nextPartialResult:
|
||||
(Downstream.Input, Input) throws -> Downstream.Input
|
||||
|
||||
private var result: Downstream.Input
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var finished = false
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
initialResult: Downstream.Input,
|
||||
nextPartialResult:
|
||||
@escaping (Downstream.Input, Input) throws -> Downstream.Input
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.nextPartialResult = nextPartialResult
|
||||
self.result = initialResult
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
do {
|
||||
result = try nextPartialResult(result, input)
|
||||
return downstream.receive(result)
|
||||
} catch {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
// Combine doesn't use locking in this method!
|
||||
guard case .subscribed = status else {
|
||||
return
|
||||
}
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "TryScan" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("status", status),
|
||||
("result", result)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 19.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes a given sequence of elements.
|
||||
@@ -25,11 +27,13 @@ extension Publishers {
|
||||
self.sequence = sequence
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure,
|
||||
Elements.Element == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Elements.Element == Downstream.Input
|
||||
{
|
||||
if let inner = Inner(downstream: subscriber, sequence: sequence) {
|
||||
var iterator = sequence.makeIterator()
|
||||
if iterator.next() != nil {
|
||||
let inner = Inner(downstream: subscriber, sequence: sequence)
|
||||
subscriber.receive(subscription: inner)
|
||||
} else {
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
@@ -44,66 +48,93 @@ extension Publishers.Sequence {
|
||||
private final class Inner<Downstream: Subscriber, Elements: Sequence, Failure>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Elements.Element,
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
typealias Iterator = Elements.Iterator
|
||||
typealias Element = Elements.Element
|
||||
|
||||
private var _downstream: Downstream?
|
||||
private var _sequence: Elements?
|
||||
private var _iterator: Iterator?
|
||||
private var _nextValue: Element?
|
||||
private var sequence: Elements?
|
||||
private var downstream: Downstream?
|
||||
private var iterator: Iterator
|
||||
private var next: Element?
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var recursion = false
|
||||
private var lock = UnfairLock.allocate()
|
||||
|
||||
init?(downstream: Downstream, sequence: Elements) {
|
||||
fileprivate init(downstream: Downstream, sequence: Elements) {
|
||||
self.sequence = sequence
|
||||
self.downstream = downstream
|
||||
self.iterator = sequence.makeIterator()
|
||||
next = iterator.next()
|
||||
}
|
||||
|
||||
// Early exit if the sequence is empty
|
||||
var iterator = sequence.makeIterator()
|
||||
guard iterator.next() != nil else { return nil }
|
||||
|
||||
_downstream = downstream
|
||||
_sequence = sequence
|
||||
_iterator = sequence.makeIterator()
|
||||
_nextValue = iterator.next()
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return _sequence.map(String.init(describing:)) ?? "Sequence"
|
||||
return sequence.map(String.init(describing:)) ?? "Sequence"
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: CollectionOfOne<(label: String?, value: Any)> =
|
||||
CollectionOfOne(("sequence", _sequence ?? [Element]()))
|
||||
let children =
|
||||
CollectionOfOne<Mirror.Child>(("sequence", sequence ?? [Element]()))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard downstream != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
if recursion {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
guard let downstream = _downstream else { return }
|
||||
while let downstream = self.downstream, pendingDemand > 0 {
|
||||
if let current = self.next {
|
||||
pendingDemand -= 1
|
||||
|
||||
var demand = demand
|
||||
|
||||
while demand > 0 {
|
||||
if let nextValue = _nextValue {
|
||||
demand += downstream.receive(nextValue)
|
||||
demand -= 1
|
||||
// Combine calls next() while the lock is held.
|
||||
// It is possible to engineer a custom Sequence that would cause
|
||||
// a dedlock here, but it would be something insane.
|
||||
let next = iterator.next()
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
let additionalDemand = downstream.receive(current)
|
||||
lock.lock()
|
||||
recursion = false
|
||||
pendingDemand += additionalDemand
|
||||
self.next = next
|
||||
}
|
||||
|
||||
_nextValue = _iterator?.next()
|
||||
|
||||
if _nextValue == nil {
|
||||
_downstream?.receive(completion: .finished)
|
||||
cancel()
|
||||
break
|
||||
if next == nil {
|
||||
self.downstream = nil
|
||||
self.sequence = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
_iterator = nil
|
||||
_sequence = nil
|
||||
lock.lock()
|
||||
downstream = nil
|
||||
sequence = nil
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
let inner = Inner(downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
|
||||
public func setFailureType<NewFailure: Error>(
|
||||
@@ -63,12 +62,20 @@ extension Publisher where Failure == Never {
|
||||
}
|
||||
|
||||
extension Publishers.SetFailureType {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
private let downstream: Downstream
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
@@ -82,5 +89,11 @@ extension Publishers.SetFailureType {
|
||||
}
|
||||
|
||||
var description: String { return "SetFailureType" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Publishers.Share
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Returns a publisher as a class instance.
|
||||
///
|
||||
/// The downstream subscriber receieves elements and completion states unchanged from
|
||||
/// the upstream publisher. Use this operator when you want to use
|
||||
/// reference semantics, such as storing a publisher instance in a property.
|
||||
///
|
||||
/// - Returns: A class instance that republishes its upstream publisher.
|
||||
public func share() -> Publishers.Share<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher implemented as a class, which otherwise behaves like its upstream
|
||||
/// publisher.
|
||||
public final class Share<Upstream: Publisher>: Publisher, Equatable {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private typealias MulticastSubject = PassthroughSubject<Output, Failure>
|
||||
|
||||
private let inner: Autoconnect<Multicast<Upstream, MulticastSubject>>
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.inner = upstream.multicast(subject: .init()).autoconnect()
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
inner.subscribe(subscriber)
|
||||
}
|
||||
|
||||
public static func == (lhs: Share, rhs: Share) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,8 +77,8 @@ extension Result {
|
||||
self.init(.failure(failure))
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where SubscriberType.Input == Success, SubscriberType.Failure == Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Success, Downstream.Failure == Failure
|
||||
{
|
||||
switch result {
|
||||
case .success(let value):
|
||||
@@ -115,34 +115,44 @@ extension Result {
|
||||
#endif
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Result.OCombine {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Success, Downstream.Failure == Failure
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let output: Success
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
init(value: Success, downstream: Downstream) {
|
||||
self.output = value
|
||||
self.downstream = downstream
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Once" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Once" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(output))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,6 @@ internal func catching<Input, Output, Failure: Error>(
|
||||
/// an error but returns `Result`.
|
||||
internal func catching<Input, Output>(
|
||||
_ transform: @escaping (Input) throws -> Output
|
||||
) -> (Input) -> Result<Output, Error> {
|
||||
) -> (Input) -> Result<Output, Error> {
|
||||
return { input in Result { try transform(input) } }
|
||||
}
|
||||
|
||||
@@ -13,21 +13,24 @@ extension Subscribers {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
public private(set) var object: Root?
|
||||
|
||||
public let keyPath: ReferenceWritableKeyPath<Root, Input>
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
public var description: String { return "Assign \(Root.self)." }
|
||||
|
||||
public var customMirror: Mirror {
|
||||
let children: [(label: String?, value: Any)] = [
|
||||
(label: "object", value: object as Any),
|
||||
(label: "keyPath", value: keyPath),
|
||||
(label: "upstreamSubscription", value: _upstreamSubscription as Any)
|
||||
let children: [Mirror.Child] = [
|
||||
("object", object as Any),
|
||||
("keyPath", keyPath),
|
||||
("status", status as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
@@ -40,17 +43,21 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
} else {
|
||||
switch status {
|
||||
case .subscribed, .terminal:
|
||||
subscription.cancel()
|
||||
case .awaitingSubscription:
|
||||
status = .subscribed(subscription)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
public func receive(_ value: Input) -> Subscribers.Demand {
|
||||
if _upstreamSubscription != nil {
|
||||
switch status {
|
||||
case .subscribed:
|
||||
object?[keyPath: keyPath] = value
|
||||
case .awaitingSubscription, .terminal:
|
||||
break
|
||||
}
|
||||
return .none
|
||||
}
|
||||
@@ -60,8 +67,11 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
return
|
||||
}
|
||||
subscription.cancel()
|
||||
status = .terminal
|
||||
object = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,18 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
/// Requests as many values as the `Publisher` can produce.
|
||||
public static let unlimited = Demand(rawValue: .max)
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static var unlimited: Demand {
|
||||
return Demand(rawValue: .max)
|
||||
}
|
||||
|
||||
/// A demand for no items.
|
||||
///
|
||||
/// This is equivalent to `Demand.max(0)`.
|
||||
public static let none = Demand.max(0)
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static var none: Demand { return .max(0) }
|
||||
|
||||
/// Limits the maximum number of values.
|
||||
/// The `Publisher` may send fewer than the requested number.
|
||||
|
||||
@@ -15,13 +15,16 @@ extension Subscribers {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
/// The closure to execute on receipt of a value.
|
||||
public let receiveValue: (Input) -> Void
|
||||
|
||||
/// The closure to execute on completion.
|
||||
public let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
public var description: String { return "Sink" }
|
||||
|
||||
@@ -45,11 +48,12 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
} else {
|
||||
switch status {
|
||||
case .subscribed, .terminal:
|
||||
subscription.cancel()
|
||||
case .awaitingSubscription:
|
||||
status = .subscribed(subscription)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +64,15 @@ extension Subscribers {
|
||||
|
||||
public func receive(completion: Subscribers.Completion<Failure>) {
|
||||
receiveCompletion(completion)
|
||||
status = .terminal
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
return
|
||||
}
|
||||
subscription.cancel()
|
||||
status = .terminal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
//
|
||||
// DispatchQueue.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21.08.2019.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import OpenCombine
|
||||
|
||||
extension DispatchQueue {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
|
||||
///
|
||||
/// Combine extends `DispatchQueue` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
|
||||
/// e. g. when importing Foundation), you will not be able
|
||||
/// to write `DispatchQueue.SchedulerTimeType`,
|
||||
/// because Swift is unable to understand which `SchedulerTimeType`
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `DispatchQueue.OCombine.SchedulerTimeType`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public struct OCombine: Scheduler {
|
||||
|
||||
public let queue: DispatchQueue
|
||||
|
||||
public init(_ queue: DispatchQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
/// The scheduler time type used by the dispatch queue.
|
||||
public struct SchedulerTimeType: Strideable, Codable, Hashable {
|
||||
|
||||
/// The dispatch time represented by this type.
|
||||
public var dispatchTime: DispatchTime
|
||||
|
||||
/// Creates a dispatch queue time type instance.
|
||||
///
|
||||
/// - Parameter time: The dispatch time to represent.
|
||||
public init(_ time: DispatchTime) {
|
||||
dispatchTime = time
|
||||
}
|
||||
|
||||
/// Returns the distance to another dispatch queue time.
|
||||
///
|
||||
/// - Parameter other: Another dispatch queue time.
|
||||
/// - Returns: The time interval between this time and the provided time.
|
||||
public func distance(to other: SchedulerTimeType) -> Stride {
|
||||
return .nanoseconds(
|
||||
Int(other.dispatchTime.rawValue - dispatchTime.rawValue)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a dispatch queue scheduler time calculated by advancing
|
||||
/// this instance’s time by the given interval.
|
||||
///
|
||||
/// - Parameter n: A time interval to advance.
|
||||
/// - Returns: A dispatch queue time advanced by the given
|
||||
/// interval from this instance’s time.
|
||||
public func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return .init(dispatchTime + stride.timeInterval)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(dispatchTime.rawValue)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(dispatchTime.uptimeNanoseconds)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
dispatchTime = try .init(uptimeNanoseconds: container.decode(UInt64.self))
|
||||
}
|
||||
|
||||
/// A type that represents the distance between two values.
|
||||
public struct Stride: SchedulerTimeIntervalConvertible,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
ExpressibleByFloatLiteral,
|
||||
Hashable,
|
||||
Codable {
|
||||
|
||||
/// If created via floating point literal, the value is
|
||||
/// converted to nanoseconds via multiplication.
|
||||
public typealias FloatLiteralType = Double
|
||||
|
||||
/// Nanoseconds, same as DispatchTimeInterval.
|
||||
public typealias IntegerLiteralType = Int
|
||||
|
||||
/// A type that can represent the absolute value of any possible
|
||||
/// value of the conforming type.
|
||||
public typealias Magnitude = Int
|
||||
|
||||
/// The value of this time interval in nanoseconds.
|
||||
public var magnitude: Int
|
||||
|
||||
/// A `DispatchTimeInterval` created with the value of this type
|
||||
/// in nanoseconds.
|
||||
public var timeInterval: DispatchTimeInterval {
|
||||
return .nanoseconds(magnitude)
|
||||
}
|
||||
|
||||
private init(magnitude: Int) {
|
||||
self.magnitude = magnitude
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from the given
|
||||
/// dispatch time interval.
|
||||
///
|
||||
/// - Parameter timeInterval: A dispatch time interval.
|
||||
public init(_ timeInterval: DispatchTimeInterval) {
|
||||
switch timeInterval {
|
||||
case .seconds(let seconds):
|
||||
self = .seconds(seconds)
|
||||
case .milliseconds(let milliseconds):
|
||||
self = .milliseconds(milliseconds)
|
||||
case .microseconds(let microseconds):
|
||||
self = .microseconds(microseconds)
|
||||
case .nanoseconds(let nanoseconds):
|
||||
self = .nanoseconds(nanoseconds)
|
||||
case .never:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
self = .nanoseconds(.max)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a floating-point
|
||||
/// seconds value.
|
||||
///
|
||||
/// - Parameter value: The number of seconds, as a `Double`.
|
||||
public init(floatLiteral value: Double) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from an integer seconds value.
|
||||
///
|
||||
/// - Parameter value: The number of seconds, as an `Int`.
|
||||
public init(integerLiteral value: Int) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a binary integer type.
|
||||
///
|
||||
/// If `exactly` cannot convert to an `Int`, the resulting time interval
|
||||
/// is `nil`.
|
||||
///
|
||||
/// - Parameter exactly: A binary integer representing a time interval.
|
||||
public init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let value = Int(exactly: source) else { return nil }
|
||||
self = .nanoseconds(value)
|
||||
}
|
||||
|
||||
public static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
}
|
||||
|
||||
public static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
// swiftlint:disable shorthand_operator
|
||||
|
||||
public static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs - rhs
|
||||
}
|
||||
|
||||
public static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs * rhs
|
||||
}
|
||||
|
||||
public static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs + rhs
|
||||
}
|
||||
|
||||
// swiftlint:enable shorthand_operator
|
||||
|
||||
public static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000_000)
|
||||
}
|
||||
|
||||
public static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000)
|
||||
}
|
||||
|
||||
public static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000)
|
||||
}
|
||||
|
||||
public static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options that affect the operation of the dispatch queue scheduler.
|
||||
public struct SchedulerOptions {
|
||||
|
||||
/// The dispatch queue quality of service.
|
||||
public var qos: DispatchQoS
|
||||
|
||||
/// The dispatch queue work item flags.
|
||||
public var flags: DispatchWorkItemFlags
|
||||
|
||||
/// The dispatch group, if any, that should be used for performing actions.
|
||||
public var group: DispatchGroup?
|
||||
|
||||
public init(qos: DispatchQoS = .unspecified,
|
||||
flags: DispatchWorkItemFlags = [],
|
||||
group: DispatchGroup? = nil) {
|
||||
self.qos = qos
|
||||
self.flags = flags
|
||||
self.group = group
|
||||
}
|
||||
}
|
||||
|
||||
public var minimumTolerance: SchedulerTimeType.Stride {
|
||||
return .nanoseconds(0)
|
||||
}
|
||||
|
||||
public var now: SchedulerTimeType {
|
||||
return .init(.now())
|
||||
}
|
||||
|
||||
public func schedule(options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
let options = options ?? .init()
|
||||
queue.async(group: options.group,
|
||||
qos: options.qos,
|
||||
flags: options.flags,
|
||||
execute: action)
|
||||
}
|
||||
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
let options = options ?? .init()
|
||||
queue.asyncAfter(deadline: date.dispatchTime,
|
||||
qos: options.qos,
|
||||
flags: options.flags,
|
||||
execute: action)
|
||||
}
|
||||
|
||||
/// Performs the action at some time after the specified date, at the specified
|
||||
/// frequency, optionally taking into account tolerance if possible.
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
let options = options ?? .init()
|
||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer.setEventHandler(qos: options.qos,
|
||||
flags: options.flags,
|
||||
handler: action)
|
||||
timer.schedule(deadline: date.dispatchTime,
|
||||
repeating: interval.timeInterval,
|
||||
leeway: tolerance.timeInterval)
|
||||
timer.resume()
|
||||
return AnyCancellable(timer.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
|
||||
///
|
||||
/// Combine extends `DispatchQueue` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
|
||||
/// e. g. when importing Foundation), you will not be able
|
||||
/// to write `DispatchQueue.main.schedule { doThings() }`,
|
||||
/// because Swift is unable to understand which `schedule` method
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `DispatchQueue.main.ocombine.schedule { doThings() }`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public var ocombine: OCombine {
|
||||
return OCombine(self)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
extension DispatchQueue: OpenCombine.Scheduler {
|
||||
|
||||
public typealias SchedulerOptions = OCombine.SchedulerOptions
|
||||
|
||||
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
|
||||
|
||||
public var minimumTolerance: OCombine.SchedulerTimeType.Stride {
|
||||
return ocombine.minimumTolerance
|
||||
}
|
||||
|
||||
public var now: OCombine.SchedulerTimeType {
|
||||
return ocombine.now
|
||||
}
|
||||
|
||||
public func schedule(options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
ocombine.schedule(options: options, action)
|
||||
}
|
||||
|
||||
public func schedule(after date: OCombine.SchedulerTimeType,
|
||||
tolerance: OCombine.SchedulerTimeType.Stride,
|
||||
options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
|
||||
}
|
||||
|
||||
public func schedule(after date: OCombine.SchedulerTimeType,
|
||||
interval: OCombine.SchedulerTimeType.Stride,
|
||||
tolerance: OCombine.SchedulerTimeType.Stride,
|
||||
options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
return ocombine.schedule(after: date,
|
||||
interval: interval,
|
||||
tolerance: tolerance,
|
||||
options: options,
|
||||
action)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Subscribers.Demand.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
import OpenCombineTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += OpenCombineTests.allTests()
|
||||
|
||||
XCTMain(tests)
|
||||
@@ -16,15 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AnyCancellableTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testClosureInitialized", testClosureInitialized),
|
||||
("testCancelableInitialized", testCancelableInitialized),
|
||||
("testCancelTwice", testCancelTwice),
|
||||
("testStoreInArbitraryCollection", testStoreInArbitraryCollection),
|
||||
("testStoreInSet", testStoreInSet),
|
||||
("testIndirectCancellation", testIndirectCancellation),
|
||||
]
|
||||
|
||||
func testClosureInitialized() {
|
||||
|
||||
var fired = false
|
||||
|
||||
@@ -16,11 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AnyPublisherTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testErasePublisher", testErasePublisher),
|
||||
("testDescription", testDescription),
|
||||
]
|
||||
|
||||
private typealias Sut = AnyPublisher<Int, TestingError>
|
||||
|
||||
func testErasePublisher() {
|
||||
@@ -31,7 +26,7 @@ final class AnyPublisherTests: XCTestCase {
|
||||
XCTAssertEqual($0.combineIdentifier, subscriber.combineIdentifier)
|
||||
}
|
||||
)
|
||||
let erased = AnyPublisher(publisher)
|
||||
let erased = publisher.eraseToAnyPublisher()
|
||||
|
||||
erased.subscribe(subscriber)
|
||||
XCTAssertEqual(publisher.history, [.subscriber])
|
||||
|
||||
@@ -19,16 +19,6 @@ private typealias Sut = AnySubscriber<Int, TestingError>
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AnySubscriberTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testCombineIdentifier", testCombineIdentifier),
|
||||
("testDescription", testDescription),
|
||||
("testReflection", testReflection),
|
||||
("testErasingSubscriber", testErasingSubscriber),
|
||||
("testErasingSubscriberSubscription", testErasingSubscriberSubscription),
|
||||
("testErasingSubject", testErasingSubject),
|
||||
("testErasingSubjectSubscription", testErasingSubjectSubscription),
|
||||
]
|
||||
|
||||
func testCombineIdentifier() {
|
||||
|
||||
let empty = Sut()
|
||||
@@ -148,18 +138,10 @@ final class AnySubscriberTests: XCTestCase {
|
||||
|
||||
let expectedEvents: [TrackingSubject<Int>.Event] =
|
||||
[.subscription("Subject")] + events.compactMap(subscriberEventToSubjectEvent)
|
||||
.throughFirstCompletion()
|
||||
|
||||
XCTAssertEqual(subject.history, expectedEvents)
|
||||
|
||||
let shuffledEvents = events.shuffled()
|
||||
|
||||
publishEvents(shuffledEvents, erased)
|
||||
|
||||
let expectedShuffledEvents =
|
||||
shuffledEvents.compactMap(subscriberEventToSubjectEvent)
|
||||
|
||||
XCTAssertEqual(subject.history, expectedEvents + expectedShuffledEvents)
|
||||
|
||||
let demand = erased.receive(0)
|
||||
|
||||
XCTAssertEqual(demand, .none)
|
||||
@@ -240,3 +222,21 @@ private func subscriberEventToSubjectEvent(
|
||||
return .completion(c)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension Array {
|
||||
func throughFirstCompletion<SubjectOutput>() -> Array
|
||||
where Element == TrackingSubject<SubjectOutput>.Event
|
||||
{
|
||||
var encounteredFirstCompletion = false
|
||||
return self.prefix {
|
||||
if encounteredFirstCompletion {
|
||||
return false
|
||||
}
|
||||
if case .completion = $0 {
|
||||
encounteredFirstCompletion = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CombineIdentifierTests: PerformanceTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDefaultInitialized", testDefaultInitialized),
|
||||
("testAnyObject", testAnyObject),
|
||||
("testDefaultInitializedPerformance", testDefaultInitializedPerformance),
|
||||
]
|
||||
|
||||
func testDefaultInitialized() {
|
||||
let id1 = CombineIdentifier()
|
||||
let id2 = CombineIdentifier()
|
||||
@@ -50,9 +44,9 @@ final class CombineIdentifierTests: PerformanceTestCase {
|
||||
}
|
||||
|
||||
func testDefaultInitializedPerformance() throws {
|
||||
try benchmark(allowFailure: isDebug, executionCount: 100) {
|
||||
try benchmark(allowFailure: isDebug, executionCount: 500) {
|
||||
for _ in 0..<2000 {
|
||||
_ = CombineIdentifier()
|
||||
blackHole(CombineIdentifier())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,9 @@ import Combine
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
// swiftlint:disable explicit_top_level_acl
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CurrentValueSubjectTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testRequestingDemand", testRequestingDemand),
|
||||
("testCrashOnZeroInitialDemand", testCrashOnZeroInitialDemand),
|
||||
("testSendFailureCompletion", testSendFailureCompletion),
|
||||
("testMultipleSubscriptions", testMultipleSubscriptions),
|
||||
("testMultipleCompletions", testMultipleCompletions),
|
||||
("testValuesAfterCompletion", testValuesAfterCompletion),
|
||||
("testSubscriptionAfterCompletion", testSubscriptionAfterCompletion),
|
||||
("testSendSubscription", testSendSubscription),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testSynchronization", testSynchronization),
|
||||
]
|
||||
|
||||
private typealias Sut = CurrentValueSubject<Int, TestingError>
|
||||
|
||||
// Reactive Streams Spec: Rules #1, #2, #9
|
||||
@@ -322,6 +307,39 @@ final class CurrentValueSubjectTests: XCTestCase {
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testSubscriptionAfterSend() {
|
||||
// Given
|
||||
let passthrough = Sut(0)
|
||||
let subscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription in
|
||||
subscription.request(.unlimited)
|
||||
})
|
||||
|
||||
// When
|
||||
passthrough.send(2)
|
||||
passthrough.subscribe(subscriber)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(subscriber.history, [.subscription("CurrentValueSubject"),
|
||||
.value(2)])
|
||||
}
|
||||
|
||||
func testSubscriptionAfterSet() {
|
||||
// Given
|
||||
let passthrough = Sut(0)
|
||||
let subscriber = TrackingSubscriber(receiveSubscription: { subscription in
|
||||
subscription.request(.unlimited)
|
||||
})
|
||||
|
||||
// When
|
||||
passthrough.value = 3
|
||||
passthrough.subscribe(subscriber)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(subscriber.history, [.subscription("CurrentValueSubject"),
|
||||
.value(3)])
|
||||
}
|
||||
|
||||
func testSendSubscription() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let cvs = Sut(1)
|
||||
@@ -441,15 +459,15 @@ final class CurrentValueSubjectTests: XCTestCase {
|
||||
|
||||
race(
|
||||
{
|
||||
cvs.value += 1
|
||||
cvs.value = 42
|
||||
},
|
||||
{
|
||||
cvs.value -= 1
|
||||
cvs.value = 42
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(inputs.value.count, 40200)
|
||||
XCTAssertEqual(cvs.value, 112)
|
||||
XCTAssertEqual(cvs.value, 42)
|
||||
|
||||
race(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//
|
||||
// DispatchQueueSchedulerTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 26.08.2019.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineDispatch
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
|
||||
// MARK: - Scheduler.SchedulerTimeType
|
||||
|
||||
func testSchedulerTimeTypeDistance() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
|
||||
|
||||
// A bug in Combine (FB7127210), caused by overflow on subtraction.
|
||||
// It should not crash. When they fix it, this test will fail and we'll know
|
||||
// that we need to update our implementation.
|
||||
assertCrashes {
|
||||
_ = time2.distance(to: time1)
|
||||
}
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let stride1 = Scheduler.SchedulerTimeType.Stride.nanoseconds(431)
|
||||
let stride2 = Scheduler.SchedulerTimeType.Stride.nanoseconds(-220)
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride1),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431)))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride2),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 9780)))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeEquatable() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time3 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10001))
|
||||
|
||||
XCTAssertEqual(time1, time1)
|
||||
XCTAssertEqual(time2, time2)
|
||||
XCTAssertEqual(time3, time3)
|
||||
|
||||
XCTAssertEqual(time1, time2)
|
||||
XCTAssertEqual(time2, time1)
|
||||
XCTAssertNotEqual(time1, time3)
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertNotEqual(time3, time1)
|
||||
}
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeHashable() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10001))
|
||||
|
||||
XCTAssertEqual(time1.hashValue, time1.dispatchTime.rawValue.hashValue)
|
||||
XCTAssertEqual(time2.hashValue, time2.dispatchTime.rawValue.hashValue)
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeCodable() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 42))
|
||||
let encodedData = try encoder
|
||||
.encode(KeyedWrapper(value: time))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":42}"#)
|
||||
|
||||
let decodedTime = try decoder
|
||||
.decode(KeyedWrapper<Scheduler.SchedulerTimeType>.self, from: encodedData)
|
||||
.value
|
||||
|
||||
XCTAssertEqual(decodedTime, time)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideToDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
switch (Stride.seconds(12).timeInterval,
|
||||
Stride.milliseconds(34).timeInterval,
|
||||
Stride.microseconds(56).timeInterval,
|
||||
Stride.nanoseconds(78).timeInterval) {
|
||||
case (.nanoseconds(12000000000),
|
||||
.nanoseconds(34000000),
|
||||
.nanoseconds(56000),
|
||||
.nanoseconds(78)):
|
||||
break // pass
|
||||
case let intervals:
|
||||
XCTFail("Unexpected DispatchTimeInterval: \(intervals)")
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideFromDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride(.seconds(12)).magnitude, 12000000000)
|
||||
XCTAssertEqual(Stride(.milliseconds(34)).magnitude, 34000000)
|
||||
XCTAssertEqual(Stride(.microseconds(56)).magnitude, 56000)
|
||||
XCTAssertEqual(Stride(.nanoseconds(78)).magnitude, 78)
|
||||
XCTAssertEqual(Stride(.never).magnitude, .max)
|
||||
}
|
||||
|
||||
func testStrideFromNumericValue() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride.seconds(12.756).magnitude, 12756000000)
|
||||
XCTAssertEqual(Stride.seconds(34).magnitude, 34000000000)
|
||||
XCTAssertEqual(Stride.milliseconds(56).magnitude, 56000000)
|
||||
XCTAssertEqual(Stride.microseconds(78).magnitude, 78000)
|
||||
XCTAssertEqual(Stride.nanoseconds(90).magnitude, 90)
|
||||
|
||||
XCTAssertEqual((12.756 as Stride).magnitude, 12756000000)
|
||||
XCTAssertEqual((34 as Stride).magnitude, 34000000000)
|
||||
|
||||
XCTAssertNil(Stride(exactly: UInt64.max))
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
|
||||
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
|
||||
XCTAssertLessThan(Stride.milliseconds(29), .seconds(29))
|
||||
}
|
||||
|
||||
func testStrideMultiplication() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) * .nanoseconds(61346)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(61346) * .nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .nanoseconds(1)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .microseconds(1)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.microseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(15) * .nanoseconds(2)).magnitude,
|
||||
30000000000)
|
||||
XCTAssertEqual((Stride.microseconds(-3) * .nanoseconds(10)).magnitude,
|
||||
-30000000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride *= .nanoseconds(61346)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(61346)
|
||||
stride *= .nanoseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .nanoseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .microseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(15)
|
||||
stride *= .nanoseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 30000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(-3)
|
||||
stride *= .nanoseconds(10)
|
||||
XCTAssertEqual(stride.magnitude, -30000000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideAddition() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude,
|
||||
2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) + .nanoseconds(7)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(-12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) + .nanoseconds(7)).magnitude,
|
||||
-5000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride += .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride += .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideSubtraction() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude,
|
||||
-2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) - .nanoseconds(7)).magnitude,
|
||||
5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(-12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) - .nanoseconds(7)).magnitude,
|
||||
-19000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride -= .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, -2000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride -= .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -19000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideCodable() throws {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let stride = Stride.nanoseconds(419872)
|
||||
let encodedData = try encoder
|
||||
.encode(KeyedWrapper(value: stride))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
|
||||
|
||||
let decodedStride = try decoder
|
||||
.decode(KeyedWrapper<Stride>.self, from: encodedData)
|
||||
.value
|
||||
|
||||
XCTAssertEqual(decodedStride, stride)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler
|
||||
|
||||
func testMinimumTolerance() {
|
||||
XCTAssertEqual(mainScheduler.minimumTolerance, .nanoseconds(0))
|
||||
XCTAssertEqual(backgroundScheduler.minimumTolerance, .nanoseconds(0))
|
||||
}
|
||||
|
||||
func testNow() {
|
||||
let expectedNow = DispatchTime.now().uptimeNanoseconds
|
||||
let actualNowMainScheduler = mainScheduler
|
||||
.now
|
||||
.dispatchTime
|
||||
.uptimeNanoseconds
|
||||
let actualNowBackgroundScheduler = backgroundScheduler
|
||||
.now
|
||||
.dispatchTime
|
||||
.uptimeNanoseconds
|
||||
XCTAssertLessThan(abs(actualNowMainScheduler.distance(to: expectedNow)),
|
||||
1_000_000/*nanoseconds*/)
|
||||
XCTAssertLessThan(abs(actualNowBackgroundScheduler.distance(to: expectedNow)),
|
||||
1_000_000/*nanoseconds*/)
|
||||
}
|
||||
|
||||
func testDefaultSchedulerOptions() {
|
||||
let options = Scheduler.SchedulerOptions()
|
||||
XCTAssertEqual(options.flags, [])
|
||||
XCTAssertEqual(options.qos, .unspecified)
|
||||
XCTAssertNil(options.group)
|
||||
}
|
||||
|
||||
func testScheduleActionOnceNow() {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteMainAction = false
|
||||
let didExecuteBackgroundAction = Atomic(false)
|
||||
|
||||
mainScheduler.schedule {
|
||||
didExecuteMainAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
backgroundScheduler
|
||||
.schedule(options: .init(qos: .userInteractive, group: group)) {
|
||||
didExecuteBackgroundAction.do { $0 = true }
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
|
||||
// Wait for the background scheduler to execute the work.
|
||||
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertTrue(didExecuteBackgroundAction.value)
|
||||
|
||||
wait(for: [main], timeout: 0.1)
|
||||
}
|
||||
|
||||
func testScheduleActionOnceLater() {
|
||||
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteAction = false
|
||||
|
||||
let delay = Scheduler.SchedulerTimeType.Stride.milliseconds(200)
|
||||
|
||||
mainScheduler.schedule(after: mainScheduler.now.advanced(by: delay)) {
|
||||
didExecuteAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteAction, "action should be executed asynchronously")
|
||||
|
||||
wait(for: [main], timeout: 3/*seconds*/)
|
||||
}
|
||||
|
||||
func testScheduleRepeating() {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.expectedFulfillmentCount = 4
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
let delay = Scheduler.SchedulerTimeType.Stride.milliseconds(100)
|
||||
let interval = Scheduler.SchedulerTimeType.Stride.milliseconds(50)
|
||||
|
||||
var didExecuteAction = false
|
||||
|
||||
let token = mainScheduler
|
||||
.schedule(after: mainScheduler.now.advanced(by: delay),
|
||||
interval: interval) {
|
||||
didExecuteAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
XCTAssert(token is AnyCancellable)
|
||||
XCTAssertFalse(didExecuteAction, "action should be executed asynchronously")
|
||||
|
||||
wait(for: [main], timeout: 3/*seconds*/)
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Scheduler = DispatchQueue
|
||||
|
||||
private let mainScheduler = DispatchQueue.main
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background)
|
||||
|
||||
#else
|
||||
|
||||
private typealias Scheduler = DispatchQueue.OCombine
|
||||
|
||||
private let mainScheduler = DispatchQueue.main.ocombine
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombine
|
||||
|
||||
#endif
|
||||
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
@@ -5,9 +5,7 @@
|
||||
// Created by Joseph Spadafora on 6/29/19.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
#if !OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// CleaningUpSubscriber.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 17.10.2019.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CleaningUpSubscriber<Input, Failure: Error>: Subscriber {
|
||||
|
||||
private(set) var subscription: Subscription?
|
||||
|
||||
private let onDeinit: () -> Void
|
||||
|
||||
init(onDeinit: @escaping () -> Void) {
|
||||
self.onDeinit = onDeinit
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
subscription = nil
|
||||
}
|
||||
}
|
||||
@@ -34,13 +34,13 @@ import OpenCombine
|
||||
typealias CustomPublisher = CustomPublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
|
||||
private(set) var subscriber: AnySubscriber<Output, Failure>?
|
||||
private(set) var erasedSubscriber: Any?
|
||||
private let subscription: Subscription?
|
||||
|
||||
init(subscription: Subscription?) {
|
||||
required init(subscription: Subscription?) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
@@ -60,3 +60,42 @@ final class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
subscriber!.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
typealias CustomConnectablePublisher = CustomConnectablePublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CustomConnectablePublisherBase<Output: Equatable, Failure: Error>
|
||||
: CustomPublisherBase<Output, Failure>,
|
||||
ConnectablePublisher
|
||||
{
|
||||
|
||||
enum Event: CustomStringConvertible {
|
||||
case connected, disconnected
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .connected:
|
||||
return ".connected"
|
||||
case .disconnected:
|
||||
return ".disconnected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Connection: Cancellable {
|
||||
|
||||
let onCancel: () -> Void
|
||||
|
||||
func cancel() {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var connectionHistory: [Event] = []
|
||||
|
||||
func connect() -> Cancellable {
|
||||
connectionHistory.append(.connected)
|
||||
return Connection { self.connectionHistory.append(.disconnected) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// OperatorTestHelper.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/6/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
/// `OperatorTestHelper` is an abstraction that helps avoid a lot of boilerplate when
|
||||
/// testing an operator. It is initialized with a publisher type and creates a
|
||||
/// `CustomSubscription`, `CustomPublisherBase` and `TrackingSubscriberBase`.
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class OperatorTestHelper<SourceValue: Equatable,
|
||||
SourceError: Error,
|
||||
SourcePublisher,
|
||||
Sut: Publisher>
|
||||
where SourcePublisher: CustomPublisherBase<SourceValue, SourceError>
|
||||
{
|
||||
typealias Value = Sut.Output
|
||||
typealias Failure = Sut.Failure
|
||||
|
||||
let subscription: CustomSubscription
|
||||
let publisher: SourcePublisher
|
||||
let tracking: TrackingSubscriberBase<Value, Failure>
|
||||
private(set) var sut: Sut
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
|
||||
/// This initializes the `OperatorTestHelper`. In most cases,
|
||||
/// you can just pass a `publisherType` and closure
|
||||
/// for `createSut` to get all the setup that you'll need for a test.
|
||||
/// - Parameter publisherType: This should be filled in with the
|
||||
/// type of `CustomPublisherBase` that you would like the
|
||||
/// operator you are testing to be built from.
|
||||
/// - Parameter initialDemand: This is the demand that the
|
||||
/// created `TrackingSubscriber` should return upon receiving a subscription.
|
||||
/// - Parameter receiveValueDemand: This is the demand that the
|
||||
/// created `TrackingSubscriber should return upon receiving a value.
|
||||
/// - Parameter customSubscription: This parameter defaults to `CustomSubscription()`,
|
||||
/// but can be replaced with your own instance if you want to override
|
||||
/// any of the default `CustomSubscription` initializer closures.
|
||||
/// - Parameter createSut: This closure takes a new concrete instance
|
||||
/// of the `publisherType` as an input to the closure and creates an
|
||||
/// instance of the operator that you are trying to test.
|
||||
init(publisherType: SourcePublisher.Type,
|
||||
initialDemand: Subscribers.Demand?,
|
||||
receiveValueDemand: Subscribers.Demand,
|
||||
customSubscription: CustomSubscription = CustomSubscription(),
|
||||
createSut: (SourcePublisher) -> Sut)
|
||||
{
|
||||
self.subscription = customSubscription
|
||||
let createdPublisher = publisherType.init(subscription: customSubscription)
|
||||
self.publisher = createdPublisher
|
||||
self.sut = createSut(createdPublisher)
|
||||
self.tracking = TrackingSubscriberBase<Value, Failure>(
|
||||
receiveSubscription: {
|
||||
initialDemand.map($0.request)
|
||||
},
|
||||
receiveValue: { _ in receiveValueDemand }
|
||||
)
|
||||
tracking.onSubscribe = { self.downstreamSubscription = $0 }
|
||||
sut.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ class PerformanceTestCase: GottaGoFast.PerformanceTestCase {
|
||||
block)
|
||||
#endif
|
||||
}
|
||||
|
||||
@inline(never)
|
||||
func blackHole<Value>(_: Value) {}
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// TestLifecycle.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
sendValue valueToBeSent: UpstreamOutput,
|
||||
cancellingSubscriptionReleasesSubscriber: Bool,
|
||||
_ makeOperator: (PassthroughSubject<UpstreamOutput, TestingError>) -> Operator
|
||||
) throws {
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
// Lifecycle test #1
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber =
|
||||
TrackingSubscriberBase<Operator.Output, Operator.Failure>(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty,
|
||||
"Lifecycle test #1: thesubscriber's history should be empty",
|
||||
file: file,
|
||||
line: line)
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
passthrough.send(valueToBeSent)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
}
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
1,
|
||||
"""
|
||||
Lifecycle test #1: deinit should be called, because \
|
||||
the subscription has completed
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
0,
|
||||
"""
|
||||
Lifecycle test #1: deinit should not be called
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
// Lifecycle test #2
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber =
|
||||
TrackingSubscriberBase<Operator.Output, Operator.Failure>(onDeinit: onDeinit)
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter,
|
||||
cancellingSubscriptionReleasesSubscriber ? 1 : 0,
|
||||
"""
|
||||
Lifecycle test #2: deinit should not be called, \
|
||||
because the subscription is never cancelled
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
// Lifecycle test #3
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber = TrackingSubscriberBase<Operator.Output, Operator.Failure>(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
passthrough.send(valueToBeSent)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter,
|
||||
cancellingSubscriptionReleasesSubscriber ? 1 : 0,
|
||||
"""
|
||||
Lifecycle test #3: deinit should not be called, \
|
||||
because the subscription is not cancelled yet
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
try XCTUnwrap(subscription, file: file, line: line).cancel()
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
2,
|
||||
"""
|
||||
Lifecycle test #3: deinit should be called, because
|
||||
the subscription has been cancelled
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
0,
|
||||
"Lifecycle test #3: deinit should not be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
// Lifecycle test #4
|
||||
|
||||
var subscriberDestroyed = false
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber = CleaningUpSubscriber<Operator.Output, Operator.Failure> {
|
||||
subscriberDestroyed = true
|
||||
}
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
passthrough.send(completion: .finished)
|
||||
}
|
||||
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// TestReflection.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
let childrenIsEmpty: (Mirror) -> Bool = { $0.children.isEmpty }
|
||||
|
||||
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
case anything
|
||||
case matches(String)
|
||||
case contains(String)
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
self = .matches(value)
|
||||
}
|
||||
}
|
||||
|
||||
func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) -> (Mirror) -> Bool {
|
||||
return { mirror in
|
||||
|
||||
let actualChildren = mirror
|
||||
.children
|
||||
.map { ($0, String(describing: $1)) }
|
||||
|
||||
for (actualChild, expectedChild) in zip(actualChildren, expectedChildren) {
|
||||
XCTAssertEqual(actualChild.0, expectedChild.0, file: file, line: line)
|
||||
switch (actualChild.1, expectedChild.1) {
|
||||
case (_, .anything):
|
||||
continue
|
||||
case let (lhs, .matches(rhs)):
|
||||
XCTAssertEqual(lhs, rhs, file: file, line: line)
|
||||
case let (lhs, .contains(rhs)):
|
||||
XCTAssert(lhs.contains(rhs),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(rhs)\"",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func reduceLikeOperatorMirror(file: StaticString = #file,
|
||||
line: UInt = #line) -> (Mirror) -> Bool {
|
||||
return expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("result", .anything),
|
||||
("initial", .anything),
|
||||
("status", .contains("awaitingSubscription")),
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
parentInput: Output.Type,
|
||||
parentFailure: Failure.Type,
|
||||
description expectedDescription: String,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
let erasedSubscriber =
|
||||
try XCTUnwrap(publisher.erasedSubscriber, file: file, line: line)
|
||||
|
||||
XCTAssertEqual((erasedSubscriber as? CustomStringConvertible)?.description,
|
||||
expectedDescription,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
"customMirror doesn't satisfy the predicate",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertFalse(erasedSubscriber is CustomDebugStringConvertible,
|
||||
"subscriber shouldn't conform to CustomDebugStringConvertible",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
XCTAssertEqual(
|
||||
((erasedSubscriber as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
playgroundDescription,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
description expectedDescription: String,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
sut: Sut
|
||||
) throws where Sut.Output: Equatable {
|
||||
let tracking = TrackingSubscriberBase<Sut.Output, Sut.Failure>()
|
||||
sut.subscribe(tracking)
|
||||
|
||||
let subscription = try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
XCTAssertEqual((subscription as? CustomStringConvertible)?.description,
|
||||
expectedDescription,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((subscription as? CustomReflectable)?.customMirror,
|
||||
"Subscription doesn't conform to CustomReflectable",
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscription is CustomReflectable,
|
||||
"Subscription shouldn't conform to CustomReflectable",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertFalse(subscription is CustomDebugStringConvertible,
|
||||
"subscriber shouldn't conform to CustomDebugStringConvertible",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
XCTAssertEqual(
|
||||
((subscription as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
playgroundDescription,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
@@ -37,36 +39,16 @@ typealias TrackingSubscriber = TrackingSubscriberBase<Int, TestingError>
|
||||
/// is considered equal to any other subscription no matter what the subscription object
|
||||
/// actually is.
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
final class TrackingSubscriberBase<Value, Failure: Error>
|
||||
: Subscriber,
|
||||
CustomStringConvertible
|
||||
{
|
||||
|
||||
enum Event: Equatable, CustomStringConvertible {
|
||||
enum Event: CustomStringConvertible {
|
||||
case subscription(StringSubscription)
|
||||
case value(Value)
|
||||
case completion(Subscribers.Completion<Failure>)
|
||||
|
||||
static func == (lhs: Event, rhs: Event) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.subscription(lhs), .subscription(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.value(lhs), .value(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.completion(lhs), .completion(rhs)):
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .subscription(let subscription):
|
||||
@@ -86,6 +68,12 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
private let _receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)?
|
||||
private let _onDeinit: (() -> Void)?
|
||||
|
||||
var onSubscribe: ((Subscription) -> Void)?
|
||||
var onValue: ((Input) -> Void)?
|
||||
var onFinish: (() -> Void)?
|
||||
var onFailure: ((Failure) -> Void)?
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
/// The history of subscriptions, inputs and completions of this subscriber
|
||||
private(set) var history: [Event] = []
|
||||
|
||||
@@ -144,16 +132,24 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
history.append(.subscription(.init(subscription)))
|
||||
onSubscribe?(subscription)
|
||||
_receiveSubscription?(subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Value) -> Subscribers.Demand {
|
||||
history.append(.value(input))
|
||||
onValue?(input)
|
||||
return _receiveValue?(input) ?? .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
history.append(.completion(completion))
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
onFailure?(error)
|
||||
case .finished:
|
||||
onFinish?()
|
||||
}
|
||||
_receiveCompletion?(completion)
|
||||
}
|
||||
|
||||
@@ -161,11 +157,69 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
return "\(type(of: self)): \(history)"
|
||||
}
|
||||
|
||||
func assertHistoryEqual(_ expected: [Event],
|
||||
valueComparator: (Value, Value) -> Bool,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
|
||||
let equals = history.count == expected.count &&
|
||||
zip(history, expected)
|
||||
.allSatisfy { $0.isEqual(to: $1, valueComparator: valueComparator) }
|
||||
|
||||
XCTAssert(equals,
|
||||
"\(history) is not equal to \(expected)",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
_onDeinit?()
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase where Value: Equatable {
|
||||
func assertHistoryEqual(_ expected: [Event],
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
assertHistoryEqual(expected, valueComparator: ==, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase.Event {
|
||||
func isEqual(to other: TrackingSubscriberBase<Value, Failure>.Event,
|
||||
valueComparator: (Value, Value) -> Bool) -> Bool {
|
||||
switch (self, other) {
|
||||
case let (.subscription(lhs), .subscription(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.value(lhs), .value(rhs)):
|
||||
return valueComparator(lhs, rhs)
|
||||
case let (.completion(lhs), .completion(rhs)):
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase.Event: Equatable where Value: Equatable {
|
||||
|
||||
static func == (lhs: TrackingSubscriberBase<Value, Failure>.Event,
|
||||
rhs: TrackingSubscriberBase<Value, Failure>.Event) -> Bool {
|
||||
return lhs.isEqual(to: rhs, valueComparator: ==)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
typealias TrackingSubject<Output: Equatable> = TrackingSubjectBase<Output, TestingError>
|
||||
|
||||
@@ -248,8 +302,8 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
_passthrough.send(completion: completion)
|
||||
}
|
||||
|
||||
func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
_receiveSubscriber?(subscriber)
|
||||
history.append(.subscriber)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
// FIXME: Remove this shim as soon is XCTUnwrap is added to swift-corelibs-xctest
|
||||
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
|
||||
|
||||
private struct UnwrappingFailure: Error {}
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ImmediateSchedulerTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testStride", testSchedulerTimeType),
|
||||
("testActions", testActions),
|
||||
]
|
||||
|
||||
func testSchedulerTimeType() throws {
|
||||
|
||||
typealias Stride = ImmediateScheduler.SchedulerTimeType.Stride
|
||||
@@ -80,5 +75,22 @@ final class ImmediateSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
XCTAssertTrue(fired)
|
||||
fired = false
|
||||
|
||||
ImmediateScheduler.shared.schedule(after: ImmediateScheduler.shared.now) {
|
||||
fired = true
|
||||
}
|
||||
|
||||
XCTAssertTrue(fired)
|
||||
fired = false
|
||||
|
||||
let cancellable = ImmediateScheduler
|
||||
.shared
|
||||
.schedule(after: ImmediateScheduler.shared.now, interval: 10) {
|
||||
fired = true
|
||||
}
|
||||
|
||||
XCTAssertTrue(fired)
|
||||
XCTAssertEqual(String(describing: cancellable), "Empty")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PassthroughSubjectTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testRequestingDemand", testRequestingDemand),
|
||||
("testCrashOnZeroInitialDemand", testCrashOnZeroInitialDemand),
|
||||
("testSendFailureCompletion", testSendFailureCompletion),
|
||||
("testMultipleSubscriptions", testMultipleSubscriptions),
|
||||
("testMultipleCompletions", testMultipleCompletions),
|
||||
("testValuesAfterCompletion", testValuesAfterCompletion),
|
||||
("testSubscriptionAfterCompletion", testSubscriptionAfterCompletion),
|
||||
("testSendSubscription", testSendSubscription),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testSynchronization", testSynchronization),
|
||||
]
|
||||
|
||||
private typealias Sut = PassthroughSubject<Int, TestingError>
|
||||
|
||||
// Reactive Streams Spec: Rules #1, #2, #9
|
||||
|
||||
@@ -16,12 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PublisherTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSubscribeSubscriber", testSubscribeSubscriber),
|
||||
("testSubscribeSubject", testSubscribeSubject),
|
||||
("testSubjectSubscriber", testSubjectSubscriber),
|
||||
]
|
||||
|
||||
func testSubscribeSubscriber() {
|
||||
|
||||
final class TrivialPublisher: Publisher {
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
//
|
||||
// AllSatisfyTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
// MARK: - AllSatisfy
|
||||
|
||||
func testAllSatisfyAllElementsSatisfyPredicate() {
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.allSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyContainsElementNotSatisfyingPredicate() {
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.allSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
{ $0.allSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.allSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.allSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testAllSatisfyRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.allSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testAllSatisfyReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: .earlyCompletion(false),
|
||||
{ $0.allSatisfy { $0 > 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: .normalCompletion(true),
|
||||
{ $0.allSatisfy { $0 == 0 } }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.allSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
func testAllSatisfyReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "AllSatisfy",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "AllSatisfy",
|
||||
{ $0.allSatisfy(AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - TryAllSatisfy
|
||||
|
||||
func testTryAllSatisfyAllElementsSatisfyPredicate() {
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.tryAllSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyContainsElementNotSatisfyingPredicate() {
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.tryAllSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testFailureBecauseOfThrow() throws {
|
||||
|
||||
func predicate(_ input: Int) throws -> Bool {
|
||||
if input == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return input < 3
|
||||
}
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryAllSatisfy",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryAllSatisfy(predicate) })
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
{ $0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryAllSatisfyRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryAllSatisfyReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: .earlyCompletion(false),
|
||||
{ $0.tryAllSatisfy { $0 > 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: .normalCompletion(true),
|
||||
{ $0.tryAllSatisfy { $0 == 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: .failure(TestingError.oops),
|
||||
{ $0.tryAllSatisfy { _ in throw TestingError.oops } }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryAllSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
func testTryAllSatisfyReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryAllSatisfy",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryAllSatisfy",
|
||||
{ $0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
/// Publishes -2, 0, 2, 4, 7
|
||||
static func testAllElementsSatisfyPredicate<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Bool,
|
||||
countPredicateCalls: Bool = true,
|
||||
_ makeOperator: (CustomPublisher, @escaping (Int) -> Bool) -> Operator
|
||||
) where Operator.Output == Bool {
|
||||
|
||||
var predicateCounter = 0
|
||||
|
||||
func predicate(_ value: Int) -> Bool {
|
||||
predicateCounter += 1
|
||||
return value.isMultiple(of: 2)
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeOperator($0, predicate) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 4)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 4)
|
||||
}
|
||||
}
|
||||
|
||||
/// Publishes -2, 0, 2, 4, 7, 8, 3
|
||||
static func testContainsElementNotSatisfyingPredicate<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Bool,
|
||||
countPredicateCalls: Bool = true,
|
||||
_ makeOperator: (CustomPublisher, @escaping (Int) -> Bool) -> Operator
|
||||
) where Operator.Output == Bool {
|
||||
|
||||
var predicateCounter = 0
|
||||
|
||||
func predicate(_ value: Int) -> Bool {
|
||||
predicateCounter += 1
|
||||
return value.isMultiple(of: 2)
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeOperator($0, predicate) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 4)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 5)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 5)
|
||||
}
|
||||
}
|
||||
|
||||
static func shouldNotBeCalled(
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) -> (Int) -> Bool {
|
||||
return { _ in
|
||||
XCTFail("Should not be called", file: file, line: line)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// AutoconnectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AutoconnectTests: XCTestCase {
|
||||
|
||||
func testBasicRefcountBehavior() throws {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomConnectablePublisher(subscription: subscription)
|
||||
|
||||
let autoconnect = publisher.autoconnect()
|
||||
|
||||
XCTAssertEqual(publisher.connectionHistory, [])
|
||||
|
||||
let subscriber1 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(101)) },
|
||||
receiveValue: { _ in .max(201) }
|
||||
)
|
||||
let subscriber2 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(102)) },
|
||||
receiveValue: { _ in .max(202) }
|
||||
)
|
||||
let subscriber3 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(103)) },
|
||||
receiveValue: { _ in .max(203) }
|
||||
)
|
||||
|
||||
autoconnect.subscribe(subscriber1) // refcount = 1
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
autoconnect.subscribe(subscriber2) // refcount = 2
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
autoconnect.subscribe(subscriber3) // refcount = 3
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
// Autoconnect should just forward events downstream
|
||||
XCTAssertEqual(publisher.send(1), .max(203))
|
||||
XCTAssertEqual(publisher.send(2), .max(203))
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(3), .max(203))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
let subscription1 = try XCTUnwrap(subscriber1.subscriptions.first?.underlying)
|
||||
let subscription2 = try XCTUnwrap(subscriber2.subscriptions.first?.underlying)
|
||||
let subscription3 = try XCTUnwrap(subscriber3.subscriptions.first?.underlying)
|
||||
|
||||
subscription2.cancel() // refcount = 2
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
subscription3.cancel() // refcount = 1
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
subscription1.cancel() // refcount = 0
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected, .disconnected])
|
||||
|
||||
// Cancelling the same subscription twice shouldn't matter
|
||||
subscription1.cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected, .disconnected])
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(101)),
|
||||
.requested(.max(102)),
|
||||
.requested(.max(103)),
|
||||
.cancelled,
|
||||
.cancelled,
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
|
||||
XCTAssertEqual(subscriber3.history, [.subscription("CustomSubscription"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.value(3),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testReentranceWhenConnecting() throws {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomConnectablePublisher(subscription: subscription)
|
||||
|
||||
let autoconnect = publisher.autoconnect()
|
||||
|
||||
let subscriber1 = TrackingSubscriber()
|
||||
|
||||
let subscriber2 = TrackingSubscriber(
|
||||
receiveSubscription: { _ in autoconnect.subscribe(subscriber1) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(publisher.connectionHistory, [])
|
||||
|
||||
autoconnect.subscribe(subscriber2)
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected])
|
||||
|
||||
try XCTUnwrap(subscriber2.subscriptions.first?.underlying).cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected,
|
||||
.disconnected])
|
||||
|
||||
try XCTUnwrap(subscriber1.subscriptions.first?.underlying).cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected,
|
||||
.disconnected])
|
||||
}
|
||||
|
||||
func testAutoconnectReflection() throws {
|
||||
|
||||
let customMirrorPredicate = expectedChildren(
|
||||
("parent", .contains("""
|
||||
Publishers.Autoconnect<\
|
||||
OpenCombineTests.\
|
||||
CustomConnectablePublisherBase<Swift.Int, \
|
||||
OpenCombineTests.TestingError>
|
||||
""")),
|
||||
("downstream", "TrackingSubscriberBase<Int, TestingError>: []")
|
||||
)
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Autoconnect",
|
||||
customMirror: customMirrorPredicate,
|
||||
playgroundDescription: "Autoconnect",
|
||||
{ $0.autoconnect() })
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let autoconnect = CustomConnectablePublisher(subscription: subscription)
|
||||
.autoconnect()
|
||||
|
||||
try testSubscriptionReflection(
|
||||
description: "CustomSubscription",
|
||||
customMirror: nil,
|
||||
playgroundDescription: "CustomSubscription",
|
||||
sut: autoconnect
|
||||
)
|
||||
|
||||
var autoconnectSubscriptionCombineID: CombineIdentifier?
|
||||
autoconnect.subscribe(
|
||||
TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
autoconnectSubscriptionCombineID = $0.combineIdentifier
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(autoconnectSubscriptionCombineID, subscription.combineIdentifier)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// CollectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CollectTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
try ReduceTests.testBasicReductionBehavior(expectedSubscription: "Collect",
|
||||
expectedResult: [1, 2, 3, 4, 5],
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Collect",
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testtestUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Collect",
|
||||
expectedResult: [],
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.collect() }
|
||||
}
|
||||
|
||||
func testRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.collect() }
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests
|
||||
.testReceiveSubscriptionTwice(expectedSubscription: "Collect",
|
||||
expectedResult: .normalCompletion([0]),
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCollectLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCollectReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Collect",
|
||||
customMirror: expectedChildren(("count", "0")),
|
||||
playgroundDescription: "Collect",
|
||||
{ $0.collect() })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
//
|
||||
// CompactMapTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.07.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CompactMapTests: XCTestCase {
|
||||
|
||||
func testEmpty() {
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = TrackingSubject<String>(
|
||||
receiveSubscriber: {
|
||||
XCTAssertEqual(String(describing: $0), "CompactMap")
|
||||
}
|
||||
)
|
||||
|
||||
publisher.compactMap(Int.init).subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap")])
|
||||
}
|
||||
|
||||
func testError() {
|
||||
let expectedError = TestingError.oops
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: CustomSubscription())
|
||||
|
||||
publisher.compactMap(Int.init).subscribe(tracking)
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testTryCompactMapFailureBecauseOfThrow() {
|
||||
var counter = 0 // How many times the transform is called?
|
||||
|
||||
let publisher = PassthroughSubject<String, Error>()
|
||||
let compactMap = publisher.tryCompactMap { value -> Int? in
|
||||
counter += 1
|
||||
if value == "throw" {
|
||||
throw "too much" as TestingError
|
||||
}
|
||||
return Int(value)
|
||||
}
|
||||
let tracking = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
publisher.send("1")
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send("2")
|
||||
publisher.send("3")
|
||||
publisher.send("throw")
|
||||
publisher.send("9")
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryCompactMap"),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure("too much" as TestingError))])
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
}
|
||||
|
||||
func testTryCompactMapFailureOnCompletion() {
|
||||
|
||||
let publisher = PassthroughSubject<String, Error>()
|
||||
let compactMap = publisher.tryCompactMap(Int.init)
|
||||
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
|
||||
publisher.send("1")
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send(completion: .failure(TestingError.oops))
|
||||
publisher.send("2")
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryCompactMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
func testRange() {
|
||||
|
||||
let publisher = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
publisher.send("1")
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send("2")
|
||||
publisher.send("a")
|
||||
publisher.send("b")
|
||||
publisher.send("a")
|
||||
publisher.send("3")
|
||||
publisher.send("4")
|
||||
publisher.send("5")
|
||||
publisher.send("!")
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send("6")
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testNoDemand() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber()
|
||||
compactMap.subscribe(tracking)
|
||||
XCTAssertTrue(subscription.history.isEmpty)
|
||||
}
|
||||
|
||||
func testDemandOnSubscribe() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
compactMap.subscribe(tracking)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(42))])
|
||||
}
|
||||
|
||||
func testDemand() {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
|
||||
var demandOnReceiveValue = Subscribers.Demand.max(3)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(5))
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { _ in demandOnReceiveValue }
|
||||
)
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 5
|
||||
|
||||
XCTAssertEqual(publisher.send("a"), .max(1))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 5
|
||||
|
||||
XCTAssertEqual(publisher.send("1"), .max(3))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 5 - 1 + 3 = 7
|
||||
|
||||
demandOnReceiveValue = .max(2)
|
||||
XCTAssertEqual(publisher.send("2"), demandOnReceiveValue)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 7 - 1 + 2 = 8
|
||||
|
||||
demandOnReceiveValue = .max(1)
|
||||
XCTAssertEqual(publisher.send("3"), demandOnReceiveValue)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 8 - 1 + 1 = 8
|
||||
|
||||
XCTAssertEqual(publisher.send("b"), .max(1))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 8
|
||||
|
||||
downstreamSubscription?.request(.max(15))
|
||||
downstreamSubscription?.request(.max(5))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5))])
|
||||
// unsatisfied demand = 8 + 15 + 5 = 28
|
||||
|
||||
demandOnReceiveValue = .none
|
||||
XCTAssertEqual(publisher.send("4"), demandOnReceiveValue)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5))])
|
||||
// unsatisfied demand = 28 - 1 + 0 = 27
|
||||
|
||||
downstreamSubscription?.request(.max(121))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121))])
|
||||
// unsatisfied demand = 27 + 121 = 148
|
||||
|
||||
XCTAssertEqual(publisher.send("c"), .max(1))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121))])
|
||||
// unsatisfied demand = 148
|
||||
|
||||
downstreamSubscription?.cancel()
|
||||
downstreamSubscription?.cancel()
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121)),
|
||||
.cancelled])
|
||||
downstreamSubscription?.request(.max(3))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121)),
|
||||
.cancelled])
|
||||
demandOnReceiveValue = .max(80)
|
||||
XCTAssertEqual(publisher.send("8"), .none)
|
||||
}
|
||||
|
||||
func testCompletion() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCompactMapCancel() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
}
|
||||
)
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(publisher.send("1"), .none)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testTryCompactMapCancel() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let tryCompactMap = publisher.tryCompactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
}
|
||||
)
|
||||
|
||||
tryCompactMap.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(publisher.send("1"), .none)
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
}
|
||||
)
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
downstreamSubscription?.request(.unlimited)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCompactMapLifecycle() throws {
|
||||
try testLifecycle(sendValue: "31",
|
||||
cancellingSubscriptionReleasesSubscriber: false) {
|
||||
$0.compactMap(Int.init)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCompactMapLifecycle() throws {
|
||||
try testLifecycle(sendValue: "31",
|
||||
cancellingSubscriptionReleasesSubscriber: false) {
|
||||
$0.tryCompactMap(Int.init)
|
||||
}
|
||||
}
|
||||
|
||||
func testCompactMapOperatorSpecializationForCompactMap() {
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = PassthroughSubject<String, TestingError>()
|
||||
|
||||
let compactMap1 = publisher.compactMap(Int.init)
|
||||
let compactMap2 = compactMap1.compactMap { $0.isMultiple(of: 2) ? $0 / 2 : nil }
|
||||
|
||||
compactMap2.subscribe(tracking)
|
||||
publisher.send("0")
|
||||
publisher.send("3")
|
||||
publisher.send("a")
|
||||
publisher.send("12")
|
||||
publisher.send("11")
|
||||
publisher.send("20")
|
||||
publisher.send("b")
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssert(compactMap1.upstream === compactMap2.upstream)
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.value(0),
|
||||
.value(6),
|
||||
.value(10),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testMapOperatorSpecializationForCompactMap() {
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = PassthroughSubject<String, TestingError>()
|
||||
|
||||
let compactMap1 = publisher.compactMap(Int.init)
|
||||
let compactMap2 = compactMap1.map { $0 + 1 }
|
||||
|
||||
compactMap2.subscribe(tracking)
|
||||
publisher.send("0")
|
||||
publisher.send("3")
|
||||
publisher.send("a")
|
||||
publisher.send("12")
|
||||
publisher.send("11")
|
||||
publisher.send("20")
|
||||
publisher.send("b")
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssert(compactMap1.upstream === compactMap2.upstream)
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.value(1),
|
||||
.value(4),
|
||||
.value(13),
|
||||
.value(12),
|
||||
.value(21),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCompactMapOperatorSpecializationForTryCompactMap() {
|
||||
let tracking = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = PassthroughSubject<String, Never>()
|
||||
|
||||
let tryCompactMap1 = publisher.tryCompactMap { input -> Int? in
|
||||
if input == "throw" { throw TestingError.oops }
|
||||
return Int(input)
|
||||
}
|
||||
|
||||
let tryCompactMap2 = tryCompactMap1
|
||||
.compactMap { $0.isMultiple(of: 2) ? $0 / 2 : nil }
|
||||
|
||||
tryCompactMap2.subscribe(tracking)
|
||||
publisher.send("0")
|
||||
publisher.send("3")
|
||||
publisher.send("a")
|
||||
publisher.send("12")
|
||||
publisher.send("11")
|
||||
publisher.send("20")
|
||||
publisher.send("b")
|
||||
|
||||
XCTAssert(tryCompactMap1.upstream === tryCompactMap2.upstream)
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryCompactMap"),
|
||||
.value(0),
|
||||
.value(6),
|
||||
.value(10)])
|
||||
|
||||
publisher.send("throw")
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryCompactMap"),
|
||||
.value(0),
|
||||
.value(6),
|
||||
.value(10),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
//
|
||||
// ComparisonTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ComparisonTests: XCTestCase {
|
||||
|
||||
// MARK: - Comparison
|
||||
|
||||
func testComparisonBasicBehavior() {
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 15,
|
||||
semantics: .max,
|
||||
countComparatorCalls: false,
|
||||
{ upstream, _ in upstream.max() }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 1,
|
||||
semantics: .min,
|
||||
countComparatorCalls: false,
|
||||
{ upstream, _ in upstream.min() }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 8,
|
||||
semantics: .max,
|
||||
{ upstream, comparator in upstream.max(by: comparator) }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 1,
|
||||
semantics: .min,
|
||||
{ upstream, comparator in upstream.min(by: comparator) }
|
||||
)
|
||||
}
|
||||
|
||||
func testComparisonUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.max() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.min() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.max() }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.min() }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.max(by: shouldNotBeCalled()) }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.min(by: shouldNotBeCalled()) }
|
||||
}
|
||||
|
||||
func testComparisonRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.max() }
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.min() }
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.max(by: shouldNotBeCalled())
|
||||
}
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.min(by: shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testComparisonReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.max() })
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.min() })
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.min(by: >) })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.max(by: >) })
|
||||
}
|
||||
|
||||
func testComparisonReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Comparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Comparison",
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Comparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Comparison",
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - TryComparison
|
||||
|
||||
func testTryComparisonBasicBehavior() {
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: 8,
|
||||
semantics: .max,
|
||||
{ upstream, comparator in upstream.tryMax(by: comparator) }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: 1,
|
||||
semantics: .min,
|
||||
{ upstream, comparator in upstream.tryMin(by: comparator) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryComparisonFailureBecauseOfThrow() throws {
|
||||
|
||||
func comparator(_ lhs: Int, _ rhs: Int) throws -> Bool {
|
||||
if lhs == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return lhs < rhs
|
||||
}
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryComparison",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryMax(by: comparator) })
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryComparison",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryMin(by: comparator) })
|
||||
}
|
||||
|
||||
func testTryComparisonUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryComparison",
|
||||
{ $0.tryMax(by: >) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryComparison",
|
||||
{ $0.tryMin(by: >) })
|
||||
}
|
||||
|
||||
func testTryComparisonUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMax(by: >) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMin(by: >) })
|
||||
}
|
||||
|
||||
func testTryComparisonCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.tryMax(by: shouldNotBeCalled()) }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.tryMin(by: shouldNotBeCalled()) }
|
||||
}
|
||||
|
||||
func testTryComparisonRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryMax(by: shouldNotBeCalled())
|
||||
}
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryMin(by: shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryComparisonReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.tryMax(by: shouldNotBeCalled()) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.tryMin(by: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryComparisonLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryMin(by: >) })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryMax(by: >) })
|
||||
}
|
||||
|
||||
func testTryComparisonReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryComparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryComparison",
|
||||
{ $0.tryMin(by: shouldNotBeCalled()) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryComparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryComparison",
|
||||
{ $0.tryMax(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private enum ComparisonSemantics {
|
||||
case min
|
||||
case max
|
||||
}
|
||||
|
||||
private struct ComparisonHistoryElement: Equatable, CustomStringConvertible {
|
||||
let lhs: Int
|
||||
let rhs: Int
|
||||
|
||||
init(_ lhs: Int, _ rhs: Int) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String { return "(\(lhs), \(rhs))" }
|
||||
}
|
||||
|
||||
/// Publishes 2, 1, 4, 6, 15, 8, `.finished`, 7, 32.
|
||||
/// Uses `Int.trailingZeroBitCount` for comparing values.
|
||||
/// Therefore, for the passed comparator 8 is max, 1 is min.
|
||||
private static func testBasicBehavior<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Int,
|
||||
semantics: ComparisonSemantics,
|
||||
countComparatorCalls: Bool = true,
|
||||
_ makeOperator: (CustomPublisher, @escaping (Int, Int) -> Bool) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
|
||||
var comparisonHistory = [ComparisonHistoryElement]()
|
||||
|
||||
func comparator(_ lhs: Int, _ rhs: Int) -> Bool {
|
||||
comparisonHistory.append(.init(lhs, rhs))
|
||||
|
||||
// Some custom logic to make sure the publisher doesn't use '<'.
|
||||
return lhs.trailingZeroBitCount < rhs.trailingZeroBitCount
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeOperator($0, comparator) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none) // trailingZeroBitCount = 1
|
||||
XCTAssertEqual(helper.publisher.send(1), .none) // trailingZeroBitCount = 0
|
||||
XCTAssertEqual(helper.publisher.send(4), .none) // trailingZeroBitCount = 2
|
||||
XCTAssertEqual(helper.publisher.send(6), .none) // trailingZeroBitCount = 1
|
||||
XCTAssertEqual(helper.publisher.send(15), .none) // trailingZeroBitCount = 0
|
||||
XCTAssertEqual(helper.publisher.send(8), .none) // trailingZeroBitCount = 3
|
||||
XCTAssertEqual(helper.publisher.send(12), .none) // trailingZeroBitCount = 2
|
||||
|
||||
if countComparatorCalls {
|
||||
switch semantics {
|
||||
case .max:
|
||||
XCTAssertEqual(comparisonHistory, [.init(2, 1),
|
||||
.init(2, 4),
|
||||
.init(4, 6),
|
||||
.init(4, 15),
|
||||
.init(4, 8),
|
||||
.init(8, 12)])
|
||||
case .min:
|
||||
XCTAssertEqual(comparisonHistory, [.init(1, 2),
|
||||
.init(4, 1),
|
||||
.init(6, 1),
|
||||
.init(15, 1),
|
||||
.init(8, 1),
|
||||
.init(12, 1)])
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .none) // trailingZeroBitCount = 0
|
||||
XCTAssertEqual(helper.publisher.send(32), .none) // trailingZeroBitCount = 5
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
if countComparatorCalls {
|
||||
XCTAssertEqual(comparisonHistory.count, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldNotBeCalled(
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) -> (Int, Int) -> Bool {
|
||||
return { _, _ in
|
||||
XCTFail("Should not be called", file: file, line: line)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
//
|
||||
// ContainsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ContainsTests: XCTestCase {
|
||||
|
||||
// MARK: - Contains
|
||||
|
||||
func testContainsAllElementsNotSatisfyPredicate() {
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: false,
|
||||
countPredicateCalls: false,
|
||||
{ upstream, _ in upstream.contains(Int.max) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsContainsElementSatisfyingPredicate() {
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: true,
|
||||
countPredicateCalls: false,
|
||||
{ upstream, _ in upstream.contains(7) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Contains",
|
||||
{ $0.contains(0) })
|
||||
}
|
||||
|
||||
func testContainsUpstreamFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(expectedSubscription: "Contains",
|
||||
expectedResult: false,
|
||||
{ $0.contains(0) })
|
||||
}
|
||||
|
||||
func testContainsCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.contains(0) }
|
||||
}
|
||||
|
||||
func testContainsRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.contains(0) }
|
||||
}
|
||||
|
||||
func testContainsReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: .earlyCompletion(true),
|
||||
{ $0.contains(0) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: .normalCompletion(false),
|
||||
{ $0.contains(1) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.contains(31) })
|
||||
}
|
||||
|
||||
func testContainsReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Contains",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Contains",
|
||||
{ $0.contains(31) })
|
||||
}
|
||||
|
||||
// MARK: - ContainsWhere
|
||||
|
||||
func testContainsWhereAllElementsNotSatisfyPredicate() {
|
||||
// ContainsWhere is just the negation of AllSatisfy
|
||||
// evaluated with negated predicate
|
||||
|
||||
// "Doesn't contain an element not satisfying the predicate"
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.contains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereContainsElementSatisfyingPredicate() {
|
||||
// ContainsWhere is just the negation of AllSatisfy
|
||||
// evaluated with negated predicate
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.contains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
{ $0.contains(where: AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereUpstreamFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: false,
|
||||
{ $0.contains(where: AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.contains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testContainsWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.contains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testContainsWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: .earlyCompletion(true),
|
||||
{ $0.contains { $0 == 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: .normalCompletion(false),
|
||||
{ $0.contains { $0 > 0 } }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.contains { _ in true } })
|
||||
}
|
||||
|
||||
func testContainsWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "ContainsWhere",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "ContainsWhere",
|
||||
{ $0.contains(where: AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - TryContainsWhere
|
||||
|
||||
func testTryContainsWhereAllElementsNotSatisfyPredicate() {
|
||||
// TryContainsWhere is just the negation of TryAllSatisfy
|
||||
// evaluated with negated predicate
|
||||
|
||||
// "Doesn't contain an element not satisfying the predicate"
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.tryContains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereContainsElementSatisfyingPredicate() {
|
||||
// TryContainsWhere is just the negation of TryAllSatisfy
|
||||
// evaluated with negated predicate
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.tryContains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testFailureBecauseOfThrow() throws {
|
||||
|
||||
func predicate(_ input: Int) throws -> Bool {
|
||||
if input == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return input > 3
|
||||
}
|
||||
|
||||
try ReduceTests
|
||||
.testFailureBecauseOfThrow(expectedSubscription: "TryContainsWhere",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryContains(where: predicate) })
|
||||
}
|
||||
|
||||
func testTryContainsWhereUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
{ $0.tryContains(where: AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereUpstreamFinishesImmediately() {
|
||||
ReduceTests .testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: false,
|
||||
{ $0.tryContains(where: AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryContainsWhereCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.tryContains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryContainsWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryContains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryContainsWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: .earlyCompletion(true),
|
||||
{ $0.tryContains { $0 == 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: .normalCompletion(false),
|
||||
{ $0.tryContains { $0 > 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: .failure(TestingError.oops),
|
||||
{ $0.tryContains { _ in throw TestingError.oops } }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryContains { _ in true } })
|
||||
}
|
||||
|
||||
func testTryContainsWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryContainsWhere",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryContainsWhere",
|
||||
{ $0.tryContains(where: AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
}
|
||||
@@ -16,135 +16,50 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CountTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSendsCorrectCount", testSendsCorrectCount),
|
||||
("testCountWaitsUntilFinishedToSend", testCountWaitsUntilFinishedToSend),
|
||||
("testAddingSubscriberRequestsUnlimitedDemand",
|
||||
testAddingSubscriberRequestsUnlimitedDemand),
|
||||
("testReceivesSubscriptionBeforeRequestingUpstream",
|
||||
testReceivesSubscriptionBeforeRequestingUpstream)
|
||||
]
|
||||
|
||||
func testSendsCorrectCount() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
let sendAmount = Int.random(in: 1...1000)
|
||||
for _ in 0..<sendAmount {
|
||||
_ = publisher.send(3)
|
||||
}
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count"),
|
||||
.value(sendAmount),
|
||||
.completion(.finished)])
|
||||
func testBasicBehavior() throws {
|
||||
try ReduceTests.testBasicReductionBehavior(expectedSubscription: "Count",
|
||||
expectedResult: 5,
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testCountWaitsUntilFinishedToSend() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
|
||||
_ = publisher.send(1)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
_ = publisher.send(2)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
_ = publisher.send(0)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count"),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
func testUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Count",
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testDemand() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(42))
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { _ in .max(4) }
|
||||
)
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(0))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(publisher.send(2), .max(0))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
downstreamSubscription?.request(.max(95))
|
||||
downstreamSubscription?.request(.max(5))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
downstreamSubscription?.cancel()
|
||||
downstreamSubscription?.cancel()
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
|
||||
downstreamSubscription?.request(.max(50))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
func testtestUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Count",
|
||||
expectedResult: 0,
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testAddingSubscriberRequestsUnlimitedDemand() {
|
||||
// When
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
// Given
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
countPublisher.subscribe(tracking)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.count() }
|
||||
}
|
||||
|
||||
func testReceivesSubscriptionBeforeRequestingUpstream() {
|
||||
let upstreamRequest = "Requested upstream subscription"
|
||||
let receiveDownstream = "Receive downstream"
|
||||
var receiveOrder: [String] = []
|
||||
func testRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.count() }
|
||||
}
|
||||
|
||||
let subscription = CustomSubscription(onRequest: { _ in
|
||||
receiveOrder.append(upstreamRequest)
|
||||
})
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { _ in
|
||||
receiveOrder.append(receiveDownstream)
|
||||
})
|
||||
func testReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests
|
||||
.testReceiveSubscriptionTwice(expectedSubscription: "Count",
|
||||
expectedResult: .normalCompletion(1),
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
func testCountLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
XCTAssertEqual(receiveOrder, [receiveDownstream, upstreamRequest])
|
||||
func testCountReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Count",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Count",
|
||||
{ $0.count() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,6 @@ import OpenCombine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DecodeTests: XCTestCase {
|
||||
static let allTests = [
|
||||
("testDecodingSuccess", testDecodingSuccess),
|
||||
("testDecodingFailure", testDecodingFailure),
|
||||
("testDemand", testDemand)
|
||||
]
|
||||
|
||||
var jsonEncoder: TestEncoder = TestEncoder()
|
||||
var jsonDecoder: TestDecoder = TestDecoder()
|
||||
@@ -34,7 +29,7 @@ final class DecodeTests: XCTestCase {
|
||||
let data = 78
|
||||
let subject = PassthroughSubject<Int, Error>()
|
||||
let publisher = subject.decode(type: [String : String].self, decoder: jsonDecoder)
|
||||
let subscriber = TrackingSubscriberBase<[String: String], Error>(
|
||||
let subscriber = TrackingSubscriberBase<[String : String], Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
jsonDecoder.handleDecode = { decodeData in
|
||||
@@ -57,7 +52,7 @@ final class DecodeTests: XCTestCase {
|
||||
let failData = 95
|
||||
let subject = PassthroughSubject<Int, Error>()
|
||||
let publisher = subject.decode(type: [String : String].self, decoder: jsonDecoder)
|
||||
let subscriber = TrackingSubscriberBase<[String: String], Error>(
|
||||
let subscriber = TrackingSubscriberBase<[String : String], Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
|
||||
@@ -17,11 +17,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DeferredTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDeferredCreatedAfterSubscription",
|
||||
testDeferredCreatedAfterSubscription)
|
||||
]
|
||||
|
||||
func testDeferredCreatedAfterSubscription() {
|
||||
var deferredPublisherCreatedCount = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// DropTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DropTests: XCTestCase {
|
||||
|
||||
func testDroppingTwoElements() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(42),
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.dropFirst(2) })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(3))
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(3))
|
||||
XCTAssertEqual(helper.publisher.send(5), .max(3))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
}
|
||||
|
||||
func testDroppingNothing() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.dropFirst(0) })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(1))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testDropNegativeNumberOfItemsCrash() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let drop = publisher.dropFirst(-1)
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
assertCrashes {
|
||||
drop.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let drop = publisher.dropFirst()
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.none) })
|
||||
assertCrashes {
|
||||
drop.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription1)
|
||||
let drop = publisher.dropFirst()
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(2)) })
|
||||
|
||||
drop.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(1)),
|
||||
.requested(.max(2))])
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription2)
|
||||
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription1)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(1)),
|
||||
.requested(.max(2)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.dropFirst() }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)), .cancelled])
|
||||
}
|
||||
|
||||
func testRequestsFromUpstreamThenSendsSubscriptionDownstream() {
|
||||
var didReceiveSubscription = false
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let drop = publisher.dropFirst()
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { _ in
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(1))])
|
||||
didReceiveSubscription = true
|
||||
}
|
||||
)
|
||||
XCTAssertFalse(didReceiveSubscription)
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
|
||||
drop.subscribe(tracking)
|
||||
|
||||
XCTAssertTrue(didReceiveSubscription)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testReusableSubscription() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.dropFirst(3) })
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(312))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(100))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(secondSubscription.history, [.requested(.max(413))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(3))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(secondSubscription.history, [.requested(.max(413))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished),
|
||||
.value(4)])
|
||||
}
|
||||
|
||||
func testLateSubscription() throws {
|
||||
|
||||
// This publisher doesn't send a subscription when it receives a subscriber
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let drop = publisher.dropFirst(4)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(10)) })
|
||||
|
||||
drop.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Drop")])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Drop")])
|
||||
}
|
||||
|
||||
func testDropLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.dropFirst(42) })
|
||||
}
|
||||
|
||||
func testDropReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "Drop",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Drop",
|
||||
{ $0.dropFirst(42) })
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DropWhileTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDropWhile", testDropWhile),
|
||||
("testTryDropWhileFailureBecauseOfThrow", testTryDropWhileFailureBecauseOfThrow),
|
||||
("testTryDropWhileFailureOnCompletion", testTryDropWhileFailureOnCompletion),
|
||||
("testTryDropWhileSuccess", testTryDropWhileSuccess),
|
||||
("testDemand", testDemand),
|
||||
("testTryDropWhileCancelsUpstreamOnThrow",
|
||||
testTryDropWhileCancelsUpstreamOnThrow),
|
||||
("testDropWhileCompletion",
|
||||
testDropWhileCompletion),
|
||||
]
|
||||
|
||||
func testDropWhile() {
|
||||
|
||||
var counter = 0 // How many times the predicate is called?
|
||||
@@ -237,16 +225,12 @@ final class DropWhileTests: XCTestCase {
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile"),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
@@ -271,64 +255,18 @@ final class DropWhileTests: XCTestCase {
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile"),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile")])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
func testDropWhileLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.drop(while: { _ in false }) })
|
||||
}
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let dropWhile = passthrough.drop(while: { _ in true })
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
dropWhile.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let dropWhile = passthrough.drop(while: { _ in true })
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
dropWhile.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let dropWhile = passthrough.drop(while: { _ in true })
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
dropWhile.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
func testTryDropWhileLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryDrop(while: { _ in false }) })
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user