103 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 46007658a1 Add missing Equatable comformances
- Collect
- Contains
- Count
- Drop
- Last
2019-10-17 14:40:56 +03:00
Sergej Jaskiewicz c4c7f2172d Add more tests for Scan, TryScan 2019-10-17 14:40:56 +03:00
Eric Patey 5b0a21a0b9 Implement Publishers.Scan 2019-10-17 14:40:56 +03:00
Sergej Jaskiewicz f4e191b2ff Add more tests for Publishers.Drop (#82) 2019-10-17 09:41:53 +03:00
Sven c275e51cdc Implement Publishers.Drop (#70) 2019-10-16 13:26:10 +03:00
Sergej Jaskiewicz a84105133c Increase timeout for DispatchQueueSchedulerTests.testScheduleActionOnceNow 2019-10-16 02:10:47 +03:00
Sergej Jaskiewicz ef3ebd965a Extract locking API into COpenCombineHelpers module 2019-10-16 02:10:47 +03:00
Sergej Jaskiewicz a08b99c886 Fix a compiler crash on Linux
This crash could only be reproduced in release configuration
2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 3398499540 Remove Unreachable.swift 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 1bf193ddaa Bump Swift version on Linux CI 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 3a88dfd76b Implemented Comparison. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz bd0b69d7cb Implement Collect. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz dba76c3c41 Implement Contains, ContainsWhere, TryContainsWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 5863492753 Implement AllSatisfy, TryAllSatisfy. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Joe Spadafora 2f2e16ee1f Implement Last, LastWhere, TryLastWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz bca131c2a4 Simplify Publishers.Count. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz e999fafdce Simplify First, FirstWhere, TryFirstWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz b38830e0f1 Implement Reduce, TryReduce. Use ReduceProducer 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 525405f64d Implement ReduceProducer
ReduceProducer is a helper class that makes implementing reduce-like
operators trivial.
2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 693d1145f8 Reenable compatibility tests (#79) 2019-10-13 20:22:03 +03:00
Sergej Jaskiewicz 2f9ddc2229 Bump Swift version on Travis for Linux (#78) 2019-10-10 10:52:18 +03:00
Sergej Jaskiewicz bcd1b727f8 Fix SwiftLint 2019-10-09 23:29:50 +03:00
Sergej Jaskiewicz 5d1034fcc0 Fix Linux build failure due to PTHREAD_MUTEX_ERRORCHECK there being Int, not Int32 2019-10-09 23:03:35 +03:00
Sergej Jaskiewicz 2378f3d97e Better error handling for pthread calls 2019-10-09 20:08:24 +03:00
Sergej Jaskiewicz 4a965830e7 Update docs to match Xcode 11.1 (#77) 2019-10-09 17:51:52 +03:00
Sergej Jaskiewicz 9eabadb7c9 Mention GYB in README.md 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz dcfaec2c9d Add tests for Publishers.MapKeyPath 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 219ee38119 Update RemainingCombineInterface.swift 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 3a5389d398 GYB cleanup 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 69ead1c8fb Fix indentation 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 8e6404592e Add .gitattributes 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 14b7ced2fe Use gyb tool to implement MapKeyPath 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz d7b9e87f6d Execute tests in parallel 2019-10-08 14:26:09 +03:00
Sergej Jaskiewicz 4fd04b8a00 Test Publishers.Print for printing to stdout 2019-10-08 14:26:09 +03:00
Sergej Jaskiewicz 5f92ee05d2 Fix the semantics to be compatible with Xcode 11.1 (#74) 2019-10-08 11:20:42 +03:00
Sven bdd703abb3 Fix Sequence cancellation (#73)
* Add test to cancel after first value received from Sequence publisher

* Stop sending values from sequence if cancelled
2019-10-07 23:19:05 +03:00
Sergej Jaskiewicz e41c48a5cd Use UInt64 as CombineIdentifier (instead of UInt) 2019-10-03 15:37:44 +03:00
Sergej Jaskiewicz df0b8b08db Add tests for Publishers.Share 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 7056143b99 Add tests for Publishers.Autoconnect 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 0a965ba60a Adopt new locking API 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 7dfaa4edea Implement Publishers.Share 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 3e8f2774a4 Implement Publishers.Autoconnect 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 68e9bbe164 Extract generation of a next CombineIdentifier to COpenCombineHelpers (#69) 2019-10-02 23:28:09 +03:00
Sergej Jaskiewicz 0f71c33d72 Add header guards 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz 3f61648f82 Use CInt instead of Int32 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz c621ceb267 Rename OpenCombineAtomics -> COpenCombineAtomics 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz 2aa297ec39 Fix access race detected by TSan 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz 9cb27bb91b Better Locking internal API 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz d74f68da86 Audit Subscribers.Assign for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz f68dcd520f Audit Subscribers.Sink for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 432fd4f48f Audit Publishers.ReplaceError for thread safety 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 9c6bbda0c4 Make Publishers.MapError.Inner a struct 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 3990ec2afb Audit Publishers.Sequence for thread safety 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 39dd9e40bf Add reflection test for Publishers.ReplaceNil 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz fd7c0459b9 Make Publishers.SetFailureType.Inner a struct 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz f7145e7fa5 Fix TryMap compatibility tests 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz ecd4766129 Audit Optional.Publisher for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz e00a6f06fc Audit Optional.Publisher for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 23ee3a4b7b Audit Just for thread safety (nothing to do there) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 9c913124eb Audit TryMap for thread safety, fix its semantics (#64) 2019-09-21 22:09:48 +03:00
Sergej Jaskiewicz 7ddd15b334 Audit Publishers.Multicast for thread safety (#63) 2019-09-20 16:15:35 +03:00
Sergej Jaskiewicz 72753ef93c Implement Publishers.MakeConnectable (#61)
* Implement Publishers.MakeConnectable

* Add MakeConnectable tests
2019-09-19 14:06:57 +03:00
Sergej Jaskiewicz 816426b48c Fix iterator.next() being called twice in Publishers.Sequence 2019-09-19 05:10:34 +03:00
Sergej Jaskiewicz 47fb390081 Add eraseToAnyPublisher() method 2019-09-18 16:55:48 +03:00
Sergej Jaskiewicz 1d3327f6bf Revert "Remove XCTUnwrap implementation"
This reverts commit 16690e6f1f.
2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz eb7478d430 Add Unreachable optimizer hint 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz f69621f0e2 Remove XCTUnwrap implementation 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 7f3cccf1ae Audit SubjectSubscriber for thread-safety 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz ec037dbb3d Fix semantics of Publishers.Print 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 8a39f35d3f Simplify Publishers.Multicast.connect() method 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 7fb92bffc6 Replace SubscriberType with Downstream in generic params 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz e441ea3048 Add missing Equatable conformances for First, ReplaceError 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 22f7b6d10d Fix code style issues with FlatMap 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 7431d21c9c Increase timeouts for DispatchQueueSchedulerTests 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 1d901fca7f Remove flaky test for DispatchQueue scheduler 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 9834eab0ea Remove some nasty unsafe code from tests
https://twitter.com/UINT_MIN/status/1168581618753163264
2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 1ce9660ce9 Remove .swiftpm 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 313d6befa6 Fix a data race in DispatchQueueSchedulerTests
Also gitignore .swiftpm and make SwiftLint happy
2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 8c7f061892 Fully cover DispatchQueue extension with tests 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 2ac2470579 Initial implementation of DispatchQueue scheduler 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 57c9ae8590 Initial implementation of DispatchQueue scheduler 2019-09-15 16:07:52 +03:00
Eric Patey d57c878651 FlatMap (#45)
Implement FlatMap
2019-09-13 10:57:17 -04:00
Eric Patey 7fa91778c2 Fix failing tests caused by Apple changes in GM Seed. (#56)
Update tests and implementation to match Apple changes in gm seed.
2019-09-13 10:50:38 -04:00
Sergej Jaskiewicz d15e604764 Remove mention of XCTestManifests from the Dangerfile 2019-09-13 14:45:28 +03:00
Evgeniy 07c7a98d72 @propertyWrapper Published (#52) 2019-09-07 18:10:53 +03:00
Sergej Jaskiewicz 01ef05be1f Pass --enable-index-store flag when testing in release mode 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz beee9d0d51 Remove allTests properties in test classes 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz aacd1a326c Remove LinuxMain.swift 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz 5528adcc67 Enable test discovery on Linux 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz 1b810d0536 Update Swift version on Linux 2019-09-07 16:32:34 +03:00
Bogdan Vlad 8b25238154 ReplaceError implementation (#50) 2019-09-01 22:05:44 +03:00
Sergej Jaskiewicz 9b9915bde7 Fix Optional.Publisher.collect() operator specialization
Update for Xcode 11.0 beta 6
2019-08-26 03:51:12 +03:00
Eric Patey 27f01e5f21 Implement IgnoreOutput (#44) 2019-08-20 17:32:33 +03:00
Sergej Jaskiewicz 739eb47409 Update for Xcode 11 beta 6
Yes, it's the only change.
2019-08-20 10:54:17 +03:00
Sergej Jaskiewicz 14d5a90e89 Add Slack badge 2019-08-05 19:57:58 +03:00
Sergej Jaskiewicz 0e869bc861 Implement CompactMap (#32) 2019-08-02 18:59:53 +03:00
Joe Spadafora 2f38069166 First where (#29) 2019-08-02 18:55:51 +03:00
Sergej Jaskiewicz 97d07d0a14 Better linting for inheritance clauses and dictionary literals 2019-08-02 14:18:17 +03:00
Joe Spadafora d3888a3808 Implement Filter/TryFilter (#22)
* Adds filter and try filter implementations

* Implement Filter

* Remove @testable declaration

* Fix linting

* Updates tests and creates testing helper

* Fix allTests to include all tests

* Renames TestHelper to OperatorTestHelper and adds documentation

* Adds more test coverage

* Updates to use subclasses for filter / tryfilter

* Adds subscription test

* Fix subscriber demand to be lazy

* Fix CustomPublisherBase changes from master

* Fix iOS availability on test helper

* Updates availability for test functions

* Simplify Filter implementation, add more tests

* Ensure test suite consistency on Darwin and Linux

* Add missing tests to XCTestManifests.swift
2019-08-02 00:20:35 +03:00
Franz Busch d2b8709afb Store newly send value in internal variable inside CurrentValueObject (#39) 2019-08-01 23:34:27 +03:00
Sergej Jaskiewicz a28177e9c5 Cache homebrew artifacts 2019-08-01 15:49:00 +03:00
Sergej Jaskiewicz cef19fce4b Use Danger CI 2019-08-01 15:49:00 +03:00
Sergej Jaskiewicz 7f6bba62de Add .hound.yml 2019-08-01 11:52:46 +03:00
136 changed files with 14464 additions and 3347 deletions
+3
View File
@@ -0,0 +1,3 @@
*.swift.gyb linguist-language=Swift
**/GENERATED-* linguist-generated=true
+111
View File
@@ -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
+32
View File
@@ -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
View File
@@ -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
+76
View File
@@ -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")
}
+45
View File
@@ -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
View File
@@ -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
)
+23 -1
View File
@@ -3,6 +3,7 @@
[![codecov](https://codecov.io/gh/broadwaylamb/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/broadwaylamb/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg)
[<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`.
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 */
+1
View File
@@ -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)?
+19 -9
View File
@@ -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 publishers 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 dont 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)
}
+3 -52
View File
@@ -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
}
}
+6 -20
View File
@@ -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))"
}
}
+13 -4
View File
@@ -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)
+28
View File
@@ -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)
}
}
}
+3 -6
View File
@@ -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
}
}
-53
View File
@@ -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()
}
}
+7 -4
View File
@@ -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()
}
+100
View File
@@ -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)
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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 }
}
}
+34 -24
View File
@@ -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 closures
/// 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 closures
/// 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 doesnt 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 doesnt 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
/// doesnt 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 elements 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 }
}
}
+1 -1
View File
@@ -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 instances time by the given interval.
///
/// - Parameter n: A time interval to advance.
/// - Returns: A dispatch queue time advanced by the given
/// interval from this instances 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
-15
View File
@@ -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 -19
View File
@@ -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