Repository: apple/swift-openapi-urlsession Branch: main Commit: 576a65b4ffb8 Files: 36 Total size: 279.3 KB Directory structure: gitextract_2owaivi_/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release.yml │ └── workflows/ │ ├── main.yml │ ├── pull_request.yml │ └── pull_request_label.yml ├── .gitignore ├── .licenseignore ├── .spi.yml ├── .swift-format ├── .swiftformatignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── README.md ├── Sources/ │ └── OpenAPIURLSession/ │ ├── BufferedStream/ │ │ ├── BufferedStream.swift │ │ └── Lock.swift │ ├── Documentation.docc/ │ │ └── Documentation.md │ ├── URLSessionBidirectionalStreaming/ │ │ ├── BidirectionalStreamingURLSessionDelegate.swift │ │ ├── HTTPBodyOutputStreamBridge.swift │ │ └── URLSession+Extensions.swift │ └── URLSessionTransport.swift └── Tests/ └── OpenAPIURLSessionTests/ ├── AsyncSyncSequence.swift ├── BufferedStreamTests/ │ └── BufferedStreamTests.swift ├── NIOAsyncHTTP1TestServer.swift ├── TaskCancellationTests.swift ├── TestUtils.swift ├── URLSessionBidirectionalStreamingTests/ │ ├── HTTPBodyOutputStreamTests.swift │ ├── MockAsyncSequence.swift │ ├── MockInputStreamDelegate.swift │ └── URLSessionBidirectionalStreamingTests.swift └── URLSessionTransportTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 🐞 Open an issue on the Swift OpenAPI Generator repository url: https://github.com/apple/swift-openapi-generator/issues about: > Issues for all repositories in the Swift OpenAPI Generator project are centralized in the swift-openapi-generator repository. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Motivation _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ ### Modifications _[Describe the modifications you've made.]_ ### Result _[After your change, what will change.]_ ### Test Plan _[Describe the steps you took, or will take, to qualify the change - such as adjusting tests and manual testing.]_ ================================================ FILE: .github/release.yml ================================================ changelog: categories: - title: SemVer Major labels: - ⚠️ semver/major - title: SemVer Minor labels: - 🆕 semver/minor - title: SemVer Patch labels: - 🔨 semver/patch - title: Other Changes labels: - semver/none ================================================ FILE: .github/workflows/main.yml ================================================ name: Main permissions: contents: read on: push: branches: [main] schedule: - cron: "0 8,20 * * *" jobs: unit-tests: name: Unit tests uses: apple/swift-nio/.github/workflows/unit_tests.yml@main with: # Disable strict concurrency checking as it intersects badly with # warnings-as-errors on 5.10 and later as SwiftPMs generated test manifest # has a non-sendable global property. # TODO: Enable warnings-as-errors on 6.0. linux_6_1_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_6_2_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_6_3_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_nightly_next_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" windows_6_1_enabled: true windows_6_2_enabled: true windows_6_3_enabled: true windows_nightly_6_1_enabled: true windows_nightly_main_enabled: true windows_6_1_arguments_override: "--explicit-target-dependency-import-check error" windows_6_2_arguments_override: "--explicit-target-dependency-import-check error" windows_6_3_arguments_override: "--explicit-target-dependency-import-check error" windows_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error" windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" macos-tests: name: macOS tests uses: apple/swift-nio/.github/workflows/macos_tests.yml@main with: runner_pool: nightly build_scheme: swift-openapi-urlsession release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main with: windows_6_1_enabled: true windows_6_2_enabled: true windows_6_3_enabled: true windows_nightly_next_enabled: true windows_nightly_main_enabled: true ================================================ FILE: .github/workflows/pull_request.yml ================================================ name: PR permissions: contents: read on: pull_request: types: [opened, reopened, synchronize] jobs: soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "SwiftOpenAPIGenerator" unit-tests: name: Unit tests uses: apple/swift-nio/.github/workflows/unit_tests.yml@main with: # Disable strict concurrency checking as it intersects badly with # warnings-as-errors on 5.10 and later as SwiftPMs generated test manifest # has a non-sendable global property. # TODO: Enable warnings-as-errors on 6.0. linux_6_1_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_6_2_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_6_3_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_nightly_next_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" windows_6_1_enabled: true windows_6_2_enabled: true windows_6_3_enabled: true windows_nightly_6_1_enabled: true windows_nightly_main_enabled: true windows_6_1_arguments_override: "--explicit-target-dependency-import-check error" windows_6_2_arguments_override: "--explicit-target-dependency-import-check error" windows_6_3_arguments_override: "--explicit-target-dependency-import-check error" windows_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error" windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" cxx-interop: name: Cxx interop uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main macos-tests: name: macOS tests uses: apple/swift-nio/.github/workflows/macos_tests.yml@main with: runner_pool: general build_scheme: swift-openapi-urlsession release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main with: windows_6_1_enabled: true windows_6_2_enabled: true windows_6_3_enabled: true windows_nightly_next_enabled: true windows_nightly_main_enabled: true ================================================ FILE: .github/workflows/pull_request_label.yml ================================================ name: PR label permissions: contents: read on: pull_request: types: [labeled, unlabeled, opened, reopened, synchronize] jobs: semver-label-check: name: Semantic version label check runs-on: ubuntu-latest timeout-minutes: 1 steps: - name: Checkout repository uses: actions/checkout@v4 with: persist-credentials: false - name: Check for Semantic Version label uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main ================================================ FILE: .gitignore ================================================ .DS_Store .build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .vscode /Package.resolved .ci/ .docc-build/ ================================================ FILE: .licenseignore ================================================ .gitignore .licenseignore .swiftformatignore .spi.yml .swift-format .github/ **.md **.txt **Package.swift Package@swift-*.swift .editorconfig ================================================ FILE: .spi.yml ================================================ version: 1 builder: configs: - documentation_targets: - OpenAPIURLSession ================================================ FILE: .swift-format ================================================ { "fileScopedDeclarationPrivacy" : { "accessLevel" : "private" }, "indentation" : { "spaces" : 4 }, "indentConditionalCompilationBlocks" : false, "indentSwitchCaseLabels" : false, "lineBreakAroundMultilineExpressionChainComponents" : true, "lineBreakBeforeControlFlowKeywords" : false, "lineBreakBeforeEachArgument" : true, "lineBreakBeforeEachGenericRequirement" : true, "lineLength" : 120, "maximumBlankLines" : 1, "prioritizeKeepingFunctionOutputTogether" : false, "respectsExistingLineBreaks" : false, "rules" : { "AllPublicDeclarationsHaveDocumentation" : true, "AlwaysUseLowerCamelCase" : false, "AmbiguousTrailingClosureOverload" : true, "BeginDocumentationCommentWithOneLineSummary" : false, "DoNotUseSemicolons" : true, "DontRepeatTypeInStaticProperties" : false, "FileScopedDeclarationPrivacy" : true, "FullyIndirectEnum" : true, "GroupNumericLiterals" : true, "IdentifiersMustBeASCII" : true, "NeverForceUnwrap" : false, "NeverUseForceTry" : false, "NeverUseImplicitlyUnwrappedOptionals" : false, "NoAccessLevelOnExtensionDeclaration" : false, "NoAssignmentInExpressions" : true, "NoBlockComments" : true, "NoCasesWithOnlyFallthrough" : true, "NoEmptyTrailingClosureParentheses" : true, "NoLabelsInCasePatterns" : false, "NoLeadingUnderscores" : false, "NoParensAroundConditions" : true, "NoVoidReturnOnFunctionSignature" : true, "OneCasePerLine" : true, "OneVariableDeclarationPerLine" : true, "OnlyOneTrailingClosureArgument" : true, "OrderedImports" : false, "ReturnVoidInsteadOfEmptyTuple" : true, "UseEarlyExits" : true, "UseLetInEveryBoundCaseVariable" : false, "UseShorthandTypeNames" : true, "UseSingleLinePropertyGetter" : false, "UseSynthesizedInitializer" : true, "UseTripleSlashForDocumentationComments" : true, "UseWhereClausesInForLoops" : false, "ValidateDocumentationComments" : true }, "spacesAroundRangeFormationOperators" : false, "tabWidth" : 8, "version" : 1 } ================================================ FILE: .swiftformatignore ================================================ **Package.swift Package@swift-*.swift ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct The code of conduct for this project can be found at https://swift.org/code-of-conduct. ================================================ FILE: CONTRIBUTING.md ================================================ ## Legal By submitting a pull request, you represent that you have the right to license your contribution to Apple and the community, and agree by submitting the patch that your contributions are licensed under the Apache 2.0 license (see `LICENSE.txt`). ## How to submit a bug report Please report any issues related to this library in the [swift-openapi-generator](https://github.com/apple/swift-openapi-generator/issues) repository. Specify the following: * Commit hash * Contextual information (e.g. what you were trying to achieve with swift-openapi-urlsession) * Simplest possible steps to reproduce * More complex the steps are, lower the priority will be. * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. * Anything that might be relevant in your opinion, such as: * Swift version or the output of `swift --version` * OS version and the output of `uname -a` * Network configuration ### Example ``` Commit hash: b17a8a9f0f814c01a56977680cb68d8a779c951f Context: While testing my application that uses with swift-openapi-urlsession, I noticed that ... Steps to reproduce: 1. ... 2. ... 3. ... 4. ... $ swift --version Swift version 4.0.2 (swift-4.0.2-RELEASE) Target: x86_64-unknown-linux-gnu Operating system: Ubuntu Linux 16.04 64-bit $ uname -a Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux My system has IPv6 disabled. ``` ## Writing a Patch A good patch is: 1. Concise, and contains as few changes as needed to achieve the end result. 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 3. Documented, adding API documentation as needed to cover new functions and properties. 4. Accompanied by a great commit message, using our commit message template. ## Running CI workflows locally You can run the Github Actions workflows locally using [act](https://github.com/nektos/act). To run all the jobs that run on a pull request, use the following command: ```bash % act pull_request ``` To run just a single job, use `workflow_call -j `, and specify the inputs the job expects. For example, to run just shellcheck: ```bash % act workflow_call -j soundness --input shell_check_enabled=true ``` To bind-mount the working directory to the container, rather than a copy, use `--bind`. For example, to run just the formatting, and have the results reflected in your working directory: ```bash % act --bind workflow_call -j soundness --input format_check_enabled=true ``` If you'd like `act` to always run with certain flags, these can be be placed in an `.actrc` file either in the current working directory or your home directory, for example: ```bash --container-architecture=linux/amd64 --remote-name upstream --action-offline-mode ``` ## How to contribute your work Please open a pull request at https://github.com/apple/swift-openapi-urlsession. Make sure the CI passes, and then wait for code review. ================================================ FILE: CONTRIBUTORS.txt ================================================ For the purpose of tracking copyright, this is the list of individuals and organizations who have contributed source code to SwiftOpenAPIGenerator. For employees of an organization/company where the copyright of work done by employees of that company is held by the company itself, only the company needs to be listed here. ## COPYRIGHT HOLDERS - Apple Inc. (all contributors with '@apple.com') ### Contributors - Honza Dvorsky - Si Beaumont **Updating this list** Please do not edit this file manually. It is generated using `./scripts/generate-contributors-list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: NOTICE.txt ================================================ The SwiftOpenAPIGenerator Project ================================= Please visit the SwiftOpenAPIGenerator web site for more information: * https://github.com/apple/swift-openapi-urlsession Copyright 2023 The SwiftOpenAPIGenerator Project The SwiftOpenAPIGenerator Project licenses this file to you under the Apache License, version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Also, please refer to each LICENSE.txt file, which is located in the 'license' directory of the distribution file, for the license terms of the components that this product depends on. ------------------------------------------------------------------------------- This product contains derivations of various scripts from SwiftNIO. * LICENSE (Apache License 2.0): * https://www.apache.org/licenses/LICENSE-2.0 * HOMEPAGE: * https://github.com/apple/swift-nio ------------------------------------------------------------------------------- This product contains AsyncSequence implementations from Swift Async Algorithms. * LICENSE (Apache License 2.0): * https://github.com/apple/swift-async-algorithms/blob/main/LICENSE.txt * HOMEPAGE: * https://github.com/apple/swift-async-algorithms ------------------------------------------------------------------------------- This product contains AsyncSequence implementations from Swift. * LICENSE (Apache License 2.0): * https://github.com/apple/swift/blob/main/LICENSE.txt * HOMEPAGE: * https://github.com/apple/swift ================================================ FILE: Package.swift ================================================ // swift-tools-version:6.1 //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import Foundation import PackageDescription // General Swift-settings for all targets. var swiftSettings: [SwiftSetting] = [ // https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md // Require `any` for existential types. .enableUpcomingFeature("ExistentialAny") ] // Strict concurrency is enabled in CI; use this environment variable to enable it locally. if ProcessInfo.processInfo.environment["SWIFT_OPENAPI_STRICT_CONCURRENCY"].flatMap(Bool.init) ?? false { swiftSettings.append(contentsOf: [ .define("SWIFT_OPENAPI_STRICT_CONCURRENCY"), .enableExperimentalFeature("StrictConcurrency"), ]) } let package = Package( name: "swift-openapi-urlsession", platforms: [.macOS(.v10_15), .macCatalyst(.v13), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)], products: [.library(name: "OpenAPIURLSession", targets: ["OpenAPIURLSession"])], dependencies: [ .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.11.0", traits: []), .package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"), .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"), ], targets: [ .target( name: "OpenAPIURLSession", dependencies: [ .product(name: "DequeModule", package: "swift-collections"), .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "HTTPTypes", package: "swift-http-types"), ], swiftSettings: swiftSettings ), .testTarget( name: "OpenAPIURLSessionTests", dependencies: ["OpenAPIURLSession"], swiftSettings: swiftSettings ), ] ) #if !os(Windows) // NIO not yet supported on Windows // Test-only dependencies. package.dependencies += [.package(url: "https://github.com/apple/swift-nio", from: "2.62.0")] package.targets.forEach { target in if target.name == "OpenAPIURLSessionTests" { target.dependencies += [.product(name: "NIOTestUtils", package: "swift-nio")] } } #endif // --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // for target in package.targets { switch target.type { case .regular, .test, .executable: var settings = target.swiftSettings ?? [] // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md settings.append(.enableUpcomingFeature("MemberImportVisibility")) target.swiftSettings = settings case .macro, .plugin, .system, .binary: () // not applicable @unknown default: () // we don't know what to do here, do nothing } } // --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // ================================================ FILE: README.md ================================================ # URLSession Transport for Swift OpenAPI Generator [![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/apple/swift-openapi-urlsession/documentation) [![](https://img.shields.io/github/v/release/apple/swift-openapi-urlsession)](https://github.com/apple/swift-openapi-urlsession/releases) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-openapi-urlsession%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/apple/swift-openapi-urlsession) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-openapi-urlsession%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/apple/swift-openapi-urlsession) A client transport that uses the [URLSession](https://developer.apple.com/documentation/foundation/urlsession) type from the [Foundation](https://developer.apple.com/documentation/foundation) framework to perform HTTP operations. Use the transport with client code generated by [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator). ## Supported platforms and minimum versions | macOS | Linux, Windows | iOS | tvOS | watchOS | visionOS | | :-: | :-: | :-: | :-: | :-: | :-: | | ✅ 10.15+ | ✅ | ✅ 13+ | ✅ 13+ | ✅ 6+ | ✅ 1+ | Note: Streaming support only available on macOS 12+, iOS 15+, tvOS 15+, watchOS 8+, and visionOS 1+. For streaming support on Linux, please use the [AsyncHTTPClient Transport](https://github.com/swift-server/swift-openapi-async-http-client) ## Usage Add the package dependency in your `Package.swift`: ```swift .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"), ``` Next, in your target, add `OpenAPIURLSession` to your dependencies: ```swift .target(name: "MyTarget", dependencies: [ .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), ]), ``` Then, to get started, check out `URLSessionTransport`. ## Documentation To learn more, check out the full [documentation](https://swiftpackageindex.com/apple/swift-openapi-urlsession/documentation). ================================================ FILE: Sources/OpenAPIURLSession/BufferedStream/BufferedStream.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// // swift-format-ignore-file //===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2020-2021 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// import DequeModule /// An asynchronous sequence generated from an error-throwing closure that /// calls a continuation to produce new elements. /// /// `BufferedStream` conforms to `AsyncSequence`, providing a convenient /// way to create an asynchronous sequence without manually implementing an /// asynchronous iterator. In particular, an asynchronous stream is well-suited /// to adapt callback- or delegation-based APIs to participate with /// `async`-`await`. /// /// In contrast to `AsyncStream`, this type can throw an error from the awaited /// `next()`, which terminates the stream with the thrown error. /// /// You initialize an `BufferedStream` with a closure that receives an /// `BufferedStream.Continuation`. Produce elements in this closure, then /// provide them to the stream by calling the continuation's `yield(_:)` method. /// When there are no further elements to produce, call the continuation's /// `finish()` method. This causes the sequence iterator to produce a `nil`, /// which terminates the sequence. If an error occurs, call the continuation's /// `finish(throwing:)` method, which causes the iterator's `next()` method to /// throw the error to the awaiting call point. The continuation is `Sendable`, /// which permits calling it from concurrent contexts external to the iteration /// of the `BufferedStream`. /// /// An arbitrary source of elements can produce elements faster than they are /// consumed by a caller iterating over them. Because of this, `BufferedStream` /// defines a buffering behavior, allowing the stream to buffer a specific /// number of oldest or newest elements. By default, the buffer limit is /// `Int.max`, which means it's unbounded. /// /// ### Adapting Existing Code to Use Streams /// /// To adapt existing callback code to use `async`-`await`, use the callbacks /// to provide values to the stream, by using the continuation's `yield(_:)` /// method. /// /// Consider a hypothetical `QuakeMonitor` type that provides callers with /// `Quake` instances every time it detects an earthquake. To receive callbacks, /// callers set a custom closure as the value of the monitor's /// `quakeHandler` property, which the monitor calls back as necessary. Callers /// can also set an `errorHandler` to receive asynchronous error notifications, /// such as the monitor service suddenly becoming unavailable. /// /// class QuakeMonitor { /// var quakeHandler: ((Quake) -> Void)? /// var errorHandler: ((Error) -> Void)? /// /// func startMonitoring() {…} /// func stopMonitoring() {…} /// } /// /// To adapt this to use `async`-`await`, extend the `QuakeMonitor` to add a /// `quakes` property, of type `BufferedStream`. In the getter for /// this property, return an `BufferedStream`, whose `build` closure -- /// called at runtime to create the stream -- uses the continuation to /// perform the following steps: /// /// 1. Creates a `QuakeMonitor` instance. /// 2. Sets the monitor's `quakeHandler` property to a closure that receives /// each `Quake` instance and forwards it to the stream by calling the /// continuation's `yield(_:)` method. /// 3. Sets the monitor's `errorHandler` property to a closure that receives /// any error from the monitor and forwards it to the stream by calling the /// continuation's `finish(throwing:)` method. This causes the stream's /// iterator to throw the error and terminate the stream. /// 4. Sets the continuation's `onTermination` property to a closure that /// calls `stopMonitoring()` on the monitor. /// 5. Calls `startMonitoring` on the `QuakeMonitor`. /// /// ``` /// extension QuakeMonitor { /// /// static var throwingQuakes: BufferedStream { /// BufferedStream { continuation in /// let monitor = QuakeMonitor() /// monitor.quakeHandler = { quake in /// continuation.yield(quake) /// } /// monitor.errorHandler = { error in /// continuation.finish(throwing: error) /// } /// continuation.onTermination = { @Sendable _ in /// monitor.stopMonitoring() /// } /// monitor.startMonitoring() /// } /// } /// } /// ``` /// /// /// Because the stream is an `AsyncSequence`, the call point uses the /// `for`-`await`-`in` syntax to process each `Quake` instance as produced by the stream: /// /// do { /// for try await quake in quakeStream { /// print("Quake: \(quake.date)") /// } /// print("Stream done.") /// } catch { /// print("Error: \(error)") /// } /// @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline internal struct BufferedStream { @usableFromInline final class _Backing: Sendable { @usableFromInline let storage: _BackPressuredStorage @usableFromInline init(storage: _BackPressuredStorage) { self.storage = storage } deinit { self.storage.sequenceDeinitialized() } } @usableFromInline enum _Implementation: Sendable { /// This is the implementation with backpressure based on the Source case backpressured(_Backing) } @usableFromInline let implementation: _Implementation } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream: AsyncSequence { /// The asynchronous iterator for iterating an asynchronous stream. /// /// This type is not `Sendable`. Don't use it from multiple /// concurrent contexts. It is a programmer error to invoke `next()` from a /// concurrent context that contends with another such call, which /// results in a call to `fatalError()`. @usableFromInline internal struct Iterator: AsyncIteratorProtocol { @usableFromInline final class _Backing { @usableFromInline let storage: _BackPressuredStorage @usableFromInline init(storage: _BackPressuredStorage) { self.storage = storage self.storage.iteratorInitialized() } deinit { self.storage.iteratorDeinitialized() } } @usableFromInline enum _Implementation { /// This is the implementation with backpressure based on the Source case backpressured(_Backing) } @usableFromInline var implementation: _Implementation @usableFromInline init(implementation: _Implementation) { self.implementation = implementation } /// The next value from the asynchronous stream. /// /// When `next()` returns `nil`, this signifies the end of the /// `BufferedStream`. /// /// It is a programmer error to invoke `next()` from a concurrent context /// that contends with another such call, which results in a call to /// `fatalError()`. /// /// If you cancel the task this iterator is running in while `next()` is /// awaiting a value, the `BufferedStream` terminates. In this case, /// `next()` may return `nil` immediately, or else return `nil` on /// subsequent calls. @inlinable internal mutating func next() async throws -> Element? { switch self.implementation { case .backpressured(let backing): return try await backing.storage.next() } } } /// Creates the asynchronous iterator that produces elements of this /// asynchronous sequence. @inlinable internal func makeAsyncIterator() -> Iterator { switch self.implementation { case .backpressured(let backing): return Iterator(implementation: .backpressured(.init(storage: backing.storage))) } } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream: Sendable where Element: Sendable {} @usableFromInline internal struct _ManagedCriticalState: @unchecked Sendable { @usableFromInline let lock: LockedValueBox @usableFromInline internal init(_ initial: State) { self.lock = .init(initial) } @inlinable internal func withCriticalRegion( _ critical: (inout State) throws -> R ) rethrows -> R { try self.lock.withLockedValue(critical) } } @usableFromInline internal struct AlreadyFinishedError: Error { @usableFromInline init() {} } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream { /// A mechanism to interface between producer code and an asynchronous stream. /// /// Use this source to provide elements to the stream by calling one of the `write` methods, then terminate the stream normally /// by calling the `finish()` method. You can also use the source's `finish(throwing:)` method to terminate the stream by /// throwing an error. @usableFromInline internal struct Source: Sendable { /// A strategy that handles the backpressure of the asynchronous stream. @usableFromInline internal struct BackPressureStrategy: Sendable { /// When the high watermark is reached producers will be suspended. All producers will be resumed again once /// the low watermark is reached. The current watermark is the number of elements in the buffer. @inlinable internal static func watermark(low: Int, high: Int) -> BackPressureStrategy { BackPressureStrategy( internalBackPressureStrategy: .watermark(.init(low: low, high: high)) ) } /// When the high watermark is reached producers will be suspended. All producers will be resumed again once /// the low watermark is reached. The current watermark is computed using the given closure. static func customWatermark( low: Int, high: Int, waterLevelForElement: @escaping @Sendable (Element) -> Int ) -> BackPressureStrategy where Element: RandomAccessCollection { BackPressureStrategy( internalBackPressureStrategy: .watermark(.init(low: low, high: high, waterLevelForElement: waterLevelForElement)) ) } @usableFromInline init(internalBackPressureStrategy: _InternalBackPressureStrategy) { self._internalBackPressureStrategy = internalBackPressureStrategy } @usableFromInline let _internalBackPressureStrategy: _InternalBackPressureStrategy } /// A type that indicates the result of writing elements to the source. @frozen @usableFromInline internal enum WriteResult: Sendable { /// A token that is returned when the asynchronous stream's backpressure strategy indicated that production should /// be suspended. Use this token to enqueue a callback by calling the ``enqueueCallback(_:)`` method. @usableFromInline internal struct CallbackToken: Sendable { @usableFromInline let id: UInt @usableFromInline init(id: UInt) { self.id = id } } /// Indicates that more elements should be produced and written to the source. case produceMore /// Indicates that a callback should be enqueued. /// /// The associated token should be passed to the ``enqueueCallback(_:)`` method. case enqueueCallback(CallbackToken) } /// Backing class for the source used to hook a deinit. @usableFromInline final class _Backing: Sendable { @usableFromInline let storage: _BackPressuredStorage @usableFromInline init(storage: _BackPressuredStorage) { self.storage = storage } deinit { self.storage.sourceDeinitialized() } } /// A callback to invoke when the stream finished. /// /// The stream finishes and calls this closure in the following cases: /// - No iterator was created and the sequence was deinited /// - An iterator was created and deinited /// - After ``finish(throwing:)`` was called and all elements have been consumed /// - The consuming task got cancelled @inlinable internal var onTermination: (@Sendable () -> Void)? { set { self._backing.storage.onTermination = newValue } get { self._backing.storage.onTermination } } @usableFromInline var _backing: _Backing @usableFromInline internal init(storage: _BackPressuredStorage) { self._backing = .init(storage: storage) } /// Writes new elements to the asynchronous stream. /// /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the /// first element of the provided sequence. If the asynchronous stream already terminated then this method will throw an error /// indicating the failure. /// /// - Parameter sequence: The elements to write to the asynchronous stream. /// - Returns: The result that indicates if more elements should be produced at this time. @inlinable internal func write(contentsOf sequence: S) throws -> WriteResult where Element == S.Element, S: Sequence { try self._backing.storage.write(contentsOf: sequence) } /// Write the element to the asynchronous stream. /// /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the /// provided element. If the asynchronous stream already terminated then this method will throw an error /// indicating the failure. /// /// - Parameter element: The element to write to the asynchronous stream. /// - Returns: The result that indicates if more elements should be produced at this time. @inlinable internal func write(_ element: Element) throws -> WriteResult { try self._backing.storage.write(contentsOf: CollectionOfOne(element)) } /// Enqueues a callback that will be invoked once more elements should be produced. /// /// Call this method after ``write(contentsOf:)`` or ``write(:)`` returned ``WriteResult/enqueueCallback(_:)``. /// /// - Important: Enqueueing the same token multiple times is not allowed. /// /// - Parameters: /// - callbackToken: The callback token. /// - onProduceMore: The callback which gets invoked once more elements should be produced. @inlinable internal func enqueueCallback( callbackToken: WriteResult.CallbackToken, onProduceMore: @escaping @Sendable (Result) -> Void ) { self._backing.storage.enqueueProducer( callbackToken: callbackToken, onProduceMore: onProduceMore ) } /// Cancel an enqueued callback. /// /// Call this method to cancel a callback enqueued by the ``enqueueCallback(callbackToken:onProduceMore:)`` method. /// /// - Note: This methods supports being called before ``enqueueCallback(callbackToken:onProduceMore:)`` is called and /// will mark the passed `callbackToken` as cancelled. /// /// - Parameter callbackToken: The callback token. @inlinable internal func cancelCallback(callbackToken: WriteResult.CallbackToken) { self._backing.storage.cancelProducer(callbackToken: callbackToken) } /// Write new elements to the asynchronous stream and provide a callback which will be invoked once more elements should be produced. /// /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the /// first element of the provided sequence. If the asynchronous stream already terminated then `onProduceMore` will be invoked with /// a `Result.failure`. /// /// - Parameters: /// - sequence: The elements to write to the asynchronous stream. /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be /// invoked during the call to ``write(contentsOf:onProduceMore:)``. @inlinable internal func write( contentsOf sequence: S, onProduceMore: @escaping @Sendable (Result) -> Void ) where Element == S.Element, S: Sequence { do { let writeResult = try self.write(contentsOf: sequence) switch writeResult { case .produceMore: onProduceMore(Result.success(())) case .enqueueCallback(let callbackToken): self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) } } catch { onProduceMore(.failure(error)) } } /// Writes the element to the asynchronous stream. /// /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the /// provided element. If the asynchronous stream already terminated then `onProduceMore` will be invoked with /// a `Result.failure`. /// /// - Parameters: /// - sequence: The element to write to the asynchronous stream. /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be /// invoked during the call to ``write(_:onProduceMore:)``. @inlinable internal func write( _ element: Element, onProduceMore: @escaping @Sendable (Result) -> Void ) { self.write(contentsOf: CollectionOfOne(element), onProduceMore: onProduceMore) } /// Write new elements to the asynchronous stream. /// /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the /// first element of the provided sequence. If the asynchronous stream already terminated then this method will throw an error /// indicating the failure. /// /// This method returns once more elements should be produced. /// /// - Parameters: /// - sequence: The elements to write to the asynchronous stream. @inlinable internal func write(contentsOf sequence: S) async throws where Element == S.Element, S: Sequence { let writeResult = try { try self.write(contentsOf: sequence) }() switch writeResult { case .produceMore: return case .enqueueCallback(let callbackToken): try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in self.enqueueCallback( callbackToken: callbackToken, onProduceMore: { result in switch result { case .success(): continuation.resume(returning: ()) case .failure(let error): continuation.resume(throwing: error) } } ) } } onCancel: { self.cancelCallback(callbackToken: callbackToken) } } } /// Write new element to the asynchronous stream. /// /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the /// provided element. If the asynchronous stream already terminated then this method will throw an error /// indicating the failure. /// /// This method returns once more elements should be produced. /// /// - Parameters: /// - sequence: The element to write to the asynchronous stream. @inlinable internal func write(_ element: Element) async throws { try await self.write(contentsOf: CollectionOfOne(element)) } /// Write the elements of the asynchronous sequence to the asynchronous stream. /// /// This method returns once the provided asynchronous sequence or the the asynchronous stream finished. /// /// - Important: This method does not finish the source if consuming the upstream sequence terminated. /// /// - Parameters: /// - sequence: The elements to write to the asynchronous stream. @inlinable internal func write(contentsOf sequence: S) async throws where Element == S.Element, S: AsyncSequence { for try await element in sequence { try await self.write(contentsOf: CollectionOfOne(element)) } } /// Indicates that the production terminated. /// /// After all buffered elements are consumed the next iteration point will return `nil` or throw an error. /// /// Calling this function more than once has no effect. After calling finish, the stream enters a terminal state and doesn't accept /// new elements. /// /// - Parameters: /// - error: The error to throw, or `nil`, to finish normally. @inlinable internal func finish(throwing error: (any Error)?) { self._backing.storage.finish(error) } } /// Initializes a new ``BufferedStream`` and an ``BufferedStream/Source``. /// /// - Parameters: /// - elementType: The element type of the stream. /// - failureType: The failure type of the stream. /// - backPressureStrategy: The backpressure strategy that the stream should use. /// - Returns: A tuple containing the stream and its source. The source should be passed to the /// producer while the stream should be passed to the consumer. @inlinable internal static func makeStream( of elementType: Element.Type = Element.self, throwing failureType: any Error.Type = (any Error).self, backPressureStrategy: Source.BackPressureStrategy ) -> (`Self`, Source) where any Error == any Error { let storage = _BackPressuredStorage( backPressureStrategy: backPressureStrategy._internalBackPressureStrategy ) let source = Source(storage: storage) return (.init(storage: storage), source) } @usableFromInline init(storage: _BackPressuredStorage) { self.implementation = .backpressured(.init(storage: storage)) } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream { @usableFromInline struct _WatermarkBackPressureStrategy: Sendable { /// The low watermark where demand should start. @usableFromInline let _low: Int /// The high watermark where demand should be stopped. @usableFromInline let _high: Int /// The current watermark. @usableFromInline private(set) var _current: Int /// Function to compute the contribution to the water level for a given element. @usableFromInline let _waterLevelForElement: (@Sendable (Element) -> Int)? /// Initializes a new ``_WatermarkBackPressureStrategy``. /// /// - Parameters: /// - low: The low watermark where demand should start. /// - high: The high watermark where demand should be stopped. /// - waterLevelForElement: Function to compute the contribution to the water level for a given element. @usableFromInline init(low: Int, high: Int, waterLevelForElement: (@Sendable (Element) -> Int)? = nil) { precondition(low <= high) self._low = low self._high = high self._current = 0 self._waterLevelForElement = waterLevelForElement } @usableFromInline mutating func didYield(elements: Deque.SubSequence) -> Bool { if let waterLevelForElement = self._waterLevelForElement { self._current += elements.reduce(0) { $0 + waterLevelForElement($1) } } else { self._current += elements.count } precondition(self._current >= 0, "Watermark below zero") // We are demanding more until we reach the high watermark return self._current < self._high } @usableFromInline mutating func didConsume(elements: Deque.SubSequence) -> Bool { if let waterLevelForElement = self._waterLevelForElement { self._current -= elements.reduce(0) { $0 + waterLevelForElement($1) } } else { self._current -= elements.count } precondition(self._current >= 0, "Watermark below zero") // We start demanding again once we are below the low watermark return self._current < self._low } @usableFromInline mutating func didConsume(element: Element) -> Bool { if let waterLevelForElement = self._waterLevelForElement { self._current -= waterLevelForElement(element) } else { self._current -= 1 } precondition(self._current >= 0, "Watermark below zero") // We start demanding again once we are below the low watermark return self._current < self._low } } @usableFromInline enum _InternalBackPressureStrategy: Sendable { case watermark(_WatermarkBackPressureStrategy) @inlinable mutating func didYield(elements: Deque.SubSequence) -> Bool { switch self { case .watermark(var strategy): let result = strategy.didYield(elements: elements) self = .watermark(strategy) return result } } @usableFromInline mutating func didConsume(elements: Deque.SubSequence) -> Bool { switch self { case .watermark(var strategy): let result = strategy.didConsume(elements: elements) self = .watermark(strategy) return result } } @usableFromInline mutating func didConsume(element: Element) -> Bool { switch self { case .watermark(var strategy): let result = strategy.didConsume(element: element) self = .watermark(strategy) return result } } } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream { // We are unchecked Sendable since we are protecting our state with a lock. @usableFromInline final class _BackPressuredStorage: Sendable { /// The state machine @usableFromInline let _stateMachine: _ManagedCriticalState<_StateMachine> @usableFromInline var onTermination: (@Sendable () -> Void)? { set { self._stateMachine.withCriticalRegion { $0._onTermination = newValue } } get { self._stateMachine.withCriticalRegion { $0._onTermination } } } @usableFromInline init( backPressureStrategy: _InternalBackPressureStrategy ) { self._stateMachine = .init(.init(backPressureStrategy: backPressureStrategy)) } @inlinable func sequenceDeinitialized() { let action = self._stateMachine.withCriticalRegion { $0.sequenceDeinitialized() } switch action { case .callOnTermination(let onTermination): onTermination?() case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): for producerContinuation in producerContinuations { producerContinuation(.failure(AlreadyFinishedError())) } onTermination?() case .none: break } } @inlinable func iteratorInitialized() { self._stateMachine.withCriticalRegion { $0.iteratorInitialized() } } @inlinable func iteratorDeinitialized() { let action = self._stateMachine.withCriticalRegion { $0.iteratorDeinitialized() } switch action { case .callOnTermination(let onTermination): onTermination?() case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): for producerContinuation in producerContinuations { producerContinuation(.failure(AlreadyFinishedError())) } onTermination?() case .none: break } } @inlinable func sourceDeinitialized() { let action = self._stateMachine.withCriticalRegion { $0.sourceDeinitialized() } switch action { case .callOnTermination(let onTermination): onTermination?() case .failProducersAndCallOnTermination( let consumer, let producerContinuations, let onTermination ): consumer?.resume(returning: nil) for producerContinuation in producerContinuations { producerContinuation(.failure(AlreadyFinishedError())) } onTermination?() case .failProducers(let producerContinuations): for producerContinuation in producerContinuations { producerContinuation(.failure(AlreadyFinishedError())) } case .none: break } } @inlinable func write( contentsOf sequence: some Sequence ) throws -> Source.WriteResult { let action = self._stateMachine.withCriticalRegion { return $0.write(sequence) } switch action { case .returnProduceMore: return .produceMore case .returnEnqueue(let callbackToken): return .enqueueCallback(callbackToken) case .resumeConsumerAndReturnProduceMore(let continuation, let element): continuation.resume(returning: element) return .produceMore case .resumeConsumerAndReturnEnqueue(let continuation, let element, let callbackToken): continuation.resume(returning: element) return .enqueueCallback(callbackToken) case .throwFinishedError: throw AlreadyFinishedError() } } @inlinable func enqueueProducer( callbackToken: Source.WriteResult.CallbackToken, onProduceMore: @escaping @Sendable (Result) -> Void ) { let action = self._stateMachine.withCriticalRegion { $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) } switch action { case .resumeProducer(let onProduceMore): onProduceMore(Result.success(())) case .resumeProducerWithError(let onProduceMore, let error): onProduceMore(Result.failure(error)) case .none: break } } @inlinable func cancelProducer(callbackToken: Source.WriteResult.CallbackToken) { let action = self._stateMachine.withCriticalRegion { $0.cancelProducer(callbackToken: callbackToken) } switch action { case .resumeProducerWithCancellationError(let onProduceMore): onProduceMore(Result.failure(CancellationError())) case .none: break } } @inlinable func finish(_ failure: (any Error)?) { let action = self._stateMachine.withCriticalRegion { $0.finish(failure) } switch action { case .callOnTermination(let onTermination): onTermination?() case .resumeConsumerAndCallOnTermination( let consumerContinuation, let failure, let onTermination ): switch failure { case .some(let error): consumerContinuation.resume(throwing: error) case .none: consumerContinuation.resume(returning: nil) } onTermination?() case .resumeProducers(let producerContinuations): for producerContinuation in producerContinuations { producerContinuation(.failure(AlreadyFinishedError())) } case .none: break } } @inlinable func next() async throws -> Element? { let action = self._stateMachine.withCriticalRegion { $0.next() } switch action { case .returnElement(let element): return element case .returnElementAndResumeProducers(let element, let producerContinuations): for producerContinuation in producerContinuations { producerContinuation(Result.success(())) } return element case .returnErrorAndCallOnTermination(let failure, let onTermination): onTermination?() switch failure { case .some(let error): throw error case .none: return nil } case .returnNil: return nil case .suspendTask: return try await self.suspendNext() } } @inlinable func suspendNext() async throws -> Element? { return try await withTaskCancellationHandler { return try await withCheckedThrowingContinuation { continuation in let action = self._stateMachine.withCriticalRegion { $0.suspendNext(continuation: continuation) } switch action { case .resumeConsumerWithElement(let continuation, let element): continuation.resume(returning: element) case .resumeConsumerWithElementAndProducers( let continuation, let element, let producerContinuations ): continuation.resume(returning: element) for producerContinuation in producerContinuations { producerContinuation(Result.success(())) } case .resumeConsumerWithErrorAndCallOnTermination( let continuation, let failure, let onTermination ): switch failure { case .some(let error): continuation.resume(throwing: error) case .none: continuation.resume(returning: nil) } onTermination?() case .resumeConsumerWithNil(let continuation): continuation.resume(returning: nil) case .none: break } } } onCancel: { let action = self._stateMachine.withCriticalRegion { $0.cancelNext() } switch action { case .resumeConsumerWithCancellationErrorAndCallOnTermination( let continuation, let onTermination ): continuation.resume(throwing: CancellationError()) onTermination?() case .failProducersAndCallOnTermination( let producerContinuations, let onTermination ): for producerContinuation in producerContinuations { producerContinuation(.failure(AlreadyFinishedError())) } onTermination?() case .none: break } } } } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream { /// The state machine of the backpressured async stream. @usableFromInline struct _StateMachine { @usableFromInline enum _State { @usableFromInline struct Initial { /// The backpressure strategy. @usableFromInline var backPressureStrategy: _InternalBackPressureStrategy /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool /// The onTermination callback. @usableFromInline var onTermination: (@Sendable () -> Void)? @usableFromInline init( backPressureStrategy: _InternalBackPressureStrategy, iteratorInitialized: Bool, onTermination: (@Sendable () -> Void)? = nil ) { self.backPressureStrategy = backPressureStrategy self.iteratorInitialized = iteratorInitialized self.onTermination = onTermination } } @usableFromInline struct Streaming { /// The backpressure strategy. @usableFromInline var backPressureStrategy: _InternalBackPressureStrategy /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool /// The onTermination callback. @usableFromInline var onTermination: (@Sendable () -> Void)? /// The buffer of elements. @usableFromInline var buffer: Deque /// The optional consumer continuation. @usableFromInline var consumerContinuation: CheckedContinuation? /// The producer continuations. @usableFromInline var producerContinuations: Deque<(UInt, (Result) -> Void)> /// The producers that have been cancelled. @usableFromInline var cancelledAsyncProducers: Deque /// Indicates if we currently have outstanding demand. @usableFromInline var hasOutstandingDemand: Bool @usableFromInline init( backPressureStrategy: _InternalBackPressureStrategy, iteratorInitialized: Bool, onTermination: (@Sendable () -> Void)? = nil, buffer: Deque, consumerContinuation: CheckedContinuation? = nil, producerContinuations: Deque<(UInt, (Result) -> Void)>, cancelledAsyncProducers: Deque, hasOutstandingDemand: Bool ) { self.backPressureStrategy = backPressureStrategy self.iteratorInitialized = iteratorInitialized self.onTermination = onTermination self.buffer = buffer self.consumerContinuation = consumerContinuation self.producerContinuations = producerContinuations self.cancelledAsyncProducers = cancelledAsyncProducers self.hasOutstandingDemand = hasOutstandingDemand } } @usableFromInline struct SourceFinished { /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool /// The buffer of elements. @usableFromInline var buffer: Deque /// The failure that should be thrown after the last element has been consumed. @usableFromInline var failure: (any Error)? /// The onTermination callback. @usableFromInline var onTermination: (@Sendable () -> Void)? @usableFromInline init( iteratorInitialized: Bool, buffer: Deque, failure: (any Error)? = nil, onTermination: (@Sendable () -> Void)? ) { self.iteratorInitialized = iteratorInitialized self.buffer = buffer self.failure = failure self.onTermination = onTermination } } case initial(Initial) /// The state once either any element was yielded or `next()` was called. case streaming(Streaming) /// The state once the underlying source signalled that it is finished. case sourceFinished(SourceFinished) /// The state once there can be no outstanding demand. This can happen if: /// 1. The iterator was deinited /// 2. The underlying source finished and all buffered elements have been consumed case finished(iteratorInitialized: Bool) /// An intermediate state to avoid CoWs. case modify } /// The state machine's current state. @usableFromInline var _state: _State // The ID used for the next CallbackToken. @usableFromInline var nextCallbackTokenID: UInt = 0 @inlinable var _onTermination: (@Sendable () -> Void)? { set { switch self._state { case .initial(var initial): initial.onTermination = newValue self._state = .initial(initial) case .streaming(var streaming): streaming.onTermination = newValue self._state = .streaming(streaming) case .sourceFinished(var sourceFinished): sourceFinished.onTermination = newValue self._state = .sourceFinished(sourceFinished) case .finished: break case .modify: fatalError("AsyncStream internal inconsistency") } } get { switch self._state { case .initial(let initial): return initial.onTermination case .streaming(let streaming): return streaming.onTermination case .sourceFinished(let sourceFinished): return sourceFinished.onTermination case .finished: return nil case .modify: fatalError("AsyncStream internal inconsistency") } } } /// Initializes a new `StateMachine`. /// /// We are passing and holding the back-pressure strategy here because /// it is a customizable extension of the state machine. /// /// - Parameter backPressureStrategy: The back-pressure strategy. @usableFromInline init( backPressureStrategy: _InternalBackPressureStrategy ) { self._state = .initial( .init( backPressureStrategy: backPressureStrategy, iteratorInitialized: false ) ) } /// Generates the next callback token. @inlinable mutating func nextCallbackToken() -> Source.WriteResult.CallbackToken { let id = self.nextCallbackTokenID self.nextCallbackTokenID += 1 return .init(id: id) } /// Actions returned by `sequenceDeinitialized()`. @usableFromInline enum SequenceDeinitializedAction { /// Indicates that `onTermination` should be called. case callOnTermination((@Sendable () -> Void)?) /// Indicates that all producers should be failed and `onTermination` should be called. case failProducersAndCallOnTermination( [(Result) -> Void], (@Sendable () -> Void)? ) } @inlinable mutating func sequenceDeinitialized() -> SequenceDeinitializedAction? { switch self._state { case .initial(let initial): if initial.iteratorInitialized { // An iterator was created and we deinited the sequence. // This is an expected pattern and we just continue on normal. return .none } else { // No iterator was created so we can transition to finished right away. self._state = .finished(iteratorInitialized: false) return .callOnTermination(initial.onTermination) } case .streaming(let streaming): if streaming.iteratorInitialized { // An iterator was created and we deinited the sequence. // This is an expected pattern and we just continue on normal. return .none } else { // No iterator was created so we can transition to finished right away. self._state = .finished(iteratorInitialized: false) return .failProducersAndCallOnTermination( Array(streaming.producerContinuations.map { $0.1 }), streaming.onTermination ) } case .sourceFinished(let sourceFinished): if sourceFinished.iteratorInitialized { // An iterator was created and we deinited the sequence. // This is an expected pattern and we just continue on normal. return .none } else { // No iterator was created so we can transition to finished right away. self._state = .finished(iteratorInitialized: false) return .callOnTermination(sourceFinished.onTermination) } case .finished: // We are already finished so there is nothing left to clean up. // This is just the references dropping afterwards. return .none case .modify: fatalError("AsyncStream internal inconsistency") } } @inlinable mutating func iteratorInitialized() { switch self._state { case .initial(var initial): if initial.iteratorInitialized { // Our sequence is a unicast sequence and does not support multiple AsyncIterator's fatalError("Only a single AsyncIterator can be created") } else { // The first and only iterator was initialized. initial.iteratorInitialized = true self._state = .initial(initial) } case .streaming(var streaming): if streaming.iteratorInitialized { // Our sequence is a unicast sequence and does not support multiple AsyncIterator's fatalError("Only a single AsyncIterator can be created") } else { // The first and only iterator was initialized. streaming.iteratorInitialized = true self._state = .streaming(streaming) } case .sourceFinished(var sourceFinished): if sourceFinished.iteratorInitialized { // Our sequence is a unicast sequence and does not support multiple AsyncIterator's fatalError("Only a single AsyncIterator can be created") } else { // The first and only iterator was initialized. sourceFinished.iteratorInitialized = true self._state = .sourceFinished(sourceFinished) } case .finished(iteratorInitialized: true): // Our sequence is a unicast sequence and does not support multiple AsyncIterator's fatalError("Only a single AsyncIterator can be created") case .finished(iteratorInitialized: false): // It is strange that an iterator is created after we are finished // but it can definitely happen, e.g. // Sequence.init -> source.finish -> sequence.makeAsyncIterator self._state = .finished(iteratorInitialized: true) case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `iteratorDeinitialized()`. @usableFromInline enum IteratorDeinitializedAction { /// Indicates that `onTermination` should be called. case callOnTermination((@Sendable () -> Void)?) /// Indicates that all producers should be failed and `onTermination` should be called. case failProducersAndCallOnTermination( [(Result) -> Void], (@Sendable () -> Void)? ) } @inlinable mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { switch self._state { case .initial(let initial): if initial.iteratorInitialized { // An iterator was created and deinited. Since we only support // a single iterator we can now transition to finish. self._state = .finished(iteratorInitialized: true) return .callOnTermination(initial.onTermination) } else { // An iterator needs to be initialized before it can be deinitialized. fatalError("AsyncStream internal inconsistency") } case .streaming(let streaming): if streaming.iteratorInitialized { // An iterator was created and deinited. Since we only support // a single iterator we can now transition to finish. self._state = .finished(iteratorInitialized: true) return .failProducersAndCallOnTermination( Array(streaming.producerContinuations.map { $0.1 }), streaming.onTermination ) } else { // An iterator needs to be initialized before it can be deinitialized. fatalError("AsyncStream internal inconsistency") } case .sourceFinished(let sourceFinished): if sourceFinished.iteratorInitialized { // An iterator was created and deinited. Since we only support // a single iterator we can now transition to finish. self._state = .finished(iteratorInitialized: true) return .callOnTermination(sourceFinished.onTermination) } else { // An iterator needs to be initialized before it can be deinitialized. fatalError("AsyncStream internal inconsistency") } case .finished: // We are already finished so there is nothing left to clean up. // This is just the references dropping afterwards. return .none case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `sourceDeinitialized()`. @usableFromInline enum SourceDeinitializedAction { /// Indicates that `onTermination` should be called. case callOnTermination((() -> Void)?) /// Indicates that all producers should be failed and `onTermination` should be called. case failProducersAndCallOnTermination( CheckedContinuation?, [(Result) -> Void], (@Sendable () -> Void)? ) /// Indicates that all producers should be failed. case failProducers([(Result) -> Void]) } @inlinable mutating func sourceDeinitialized() -> SourceDeinitializedAction? { switch self._state { case .initial(let initial): // The source got deinited before anything was written self._state = .finished(iteratorInitialized: initial.iteratorInitialized) return .callOnTermination(initial.onTermination) case .streaming(let streaming): if streaming.buffer.isEmpty { // We can transition to finished right away since the buffer is empty now self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) return .failProducersAndCallOnTermination( streaming.consumerContinuation, Array(streaming.producerContinuations.map { $0.1 }), streaming.onTermination ) } else { // The continuation must be `nil` if the buffer has elements precondition(streaming.consumerContinuation == nil) self._state = .sourceFinished( .init( iteratorInitialized: streaming.iteratorInitialized, buffer: streaming.buffer, failure: nil, onTermination: streaming.onTermination ) ) return .failProducers( Array(streaming.producerContinuations.map { $0.1 }) ) } case .sourceFinished, .finished: // This is normal and we just have to tolerate it return .none case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `write()`. @usableFromInline enum WriteAction { /// Indicates that the producer should be notified to produce more. case returnProduceMore /// Indicates that the producer should be suspended to stop producing. case returnEnqueue( callbackToken: Source.WriteResult.CallbackToken ) /// Indicates that the consumer should be resumed and the producer should be notified to produce more. case resumeConsumerAndReturnProduceMore( continuation: CheckedContinuation, element: Element ) /// Indicates that the consumer should be resumed and the producer should be suspended. case resumeConsumerAndReturnEnqueue( continuation: CheckedContinuation, element: Element, callbackToken: Source.WriteResult.CallbackToken ) /// Indicates that the producer has been finished. case throwFinishedError @inlinable init( callbackToken: Source.WriteResult.CallbackToken?, continuationAndElement: (CheckedContinuation, Element)? = nil ) { switch (callbackToken, continuationAndElement) { case (.none, .none): self = .returnProduceMore case (.some(let callbackToken), .none): self = .returnEnqueue(callbackToken: callbackToken) case (.none, .some((let continuation, let element))): self = .resumeConsumerAndReturnProduceMore( continuation: continuation, element: element ) case (.some(let callbackToken), .some((let continuation, let element))): self = .resumeConsumerAndReturnEnqueue( continuation: continuation, element: element, callbackToken: callbackToken ) } } } @inlinable mutating func write(_ sequence: some Sequence) -> WriteAction { switch self._state { case .initial(var initial): var buffer = Deque() buffer.append(contentsOf: sequence) let shouldProduceMore = initial.backPressureStrategy.didYield(elements: buffer[...]) let callbackToken = shouldProduceMore ? nil : self.nextCallbackToken() self._state = .streaming( .init( backPressureStrategy: initial.backPressureStrategy, iteratorInitialized: initial.iteratorInitialized, onTermination: initial.onTermination, buffer: buffer, consumerContinuation: nil, producerContinuations: .init(), cancelledAsyncProducers: .init(), hasOutstandingDemand: shouldProduceMore ) ) return .init(callbackToken: callbackToken) case .streaming(var streaming): self._state = .modify let bufferEndIndexBeforeAppend = streaming.buffer.endIndex streaming.buffer.append(contentsOf: sequence) // We have an element and can resume the continuation streaming.hasOutstandingDemand = streaming.backPressureStrategy.didYield( elements: streaming.buffer[bufferEndIndexBeforeAppend...] ) if let consumerContinuation = streaming.consumerContinuation { guard let element = streaming.buffer.popFirst() else { // We got a yield of an empty sequence. We just tolerate this. self._state = .streaming(streaming) return .init(callbackToken: streaming.hasOutstandingDemand ? nil : self.nextCallbackToken()) } streaming.hasOutstandingDemand = streaming.backPressureStrategy.didConsume(element: element) // We got a consumer continuation and an element. We can resume the consumer now streaming.consumerContinuation = nil self._state = .streaming(streaming) return .init( callbackToken: streaming.hasOutstandingDemand ? nil : self.nextCallbackToken(), continuationAndElement: (consumerContinuation, element) ) } else { // We don't have a suspended consumer so we just buffer the elements self._state = .streaming(streaming) return .init( callbackToken: streaming.hasOutstandingDemand ? nil : self.nextCallbackToken() ) } case .sourceFinished, .finished: // If the source has finished we are dropping the elements. return .throwFinishedError case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `enqueueProducer()`. @usableFromInline enum EnqueueProducerAction { /// Indicates that the producer should be notified to produce more. case resumeProducer((Result) -> Void) /// Indicates that the producer should be notified about an error. case resumeProducerWithError((Result) -> Void, any Error) } @inlinable mutating func enqueueProducer( callbackToken: Source.WriteResult.CallbackToken, onProduceMore: @Sendable @escaping (Result) -> Void ) -> EnqueueProducerAction? { switch self._state { case .initial: // We need to transition to streaming before we can suspend // This is enforced because the CallbackToken has no internal init so // one must create it by calling `write` first. fatalError("AsyncStream internal inconsistency") case .streaming(var streaming): if let index = streaming.cancelledAsyncProducers.firstIndex(of: callbackToken.id) { // Our producer got marked as cancelled. self._state = .modify streaming.cancelledAsyncProducers.remove(at: index) self._state = .streaming(streaming) return .resumeProducerWithError(onProduceMore, CancellationError()) } else if streaming.hasOutstandingDemand { // We hit an edge case here where we wrote but the consuming thread got interleaved return .resumeProducer(onProduceMore) } else { self._state = .modify streaming.producerContinuations.append((callbackToken.id, onProduceMore)) self._state = .streaming(streaming) return .none } case .sourceFinished, .finished: // Since we are unlocking between yielding and suspending the yield // It can happen that the source got finished or the consumption fully finishes. return .resumeProducerWithError(onProduceMore, AlreadyFinishedError()) case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `cancelProducer()`. @usableFromInline enum CancelProducerAction { /// Indicates that the producer should be notified about cancellation. case resumeProducerWithCancellationError((Result) -> Void) } @inlinable mutating func cancelProducer( callbackToken: Source.WriteResult.CallbackToken ) -> CancelProducerAction? { switch self._state { case .initial: // We need to transition to streaming before we can suspend fatalError("AsyncStream internal inconsistency") case .streaming(var streaming): if let index = streaming.producerContinuations.firstIndex(where: { $0.0 == callbackToken.id }) { // We have an enqueued producer that we need to resume now self._state = .modify let continuation = streaming.producerContinuations.remove(at: index).1 self._state = .streaming(streaming) return .resumeProducerWithCancellationError(continuation) } else { // The task that yields was cancelled before yielding so the cancellation handler // got invoked right away self._state = .modify streaming.cancelledAsyncProducers.append(callbackToken.id) self._state = .streaming(streaming) return .none } case .sourceFinished, .finished: // Since we are unlocking between yielding and suspending the yield // It can happen that the source got finished or the consumption fully finishes. return .none case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `finish()`. @usableFromInline enum FinishAction { /// Indicates that `onTermination` should be called. case callOnTermination((() -> Void)?) /// Indicates that the consumer should be resumed with the failure, the producers /// should be resumed with an error and `onTermination` should be called. case resumeConsumerAndCallOnTermination( consumerContinuation: CheckedContinuation, failure: (any Error)?, onTermination: (() -> Void)? ) /// Indicates that the producers should be resumed with an error. case resumeProducers( producerContinuations: [(Result) -> Void] ) } @inlinable mutating func finish(_ failure: (any Error)?) -> FinishAction? { switch self._state { case .initial(let initial): // Nothing was yielded nor did anybody call next // This means we can transition to sourceFinished and store the failure self._state = .sourceFinished( .init( iteratorInitialized: initial.iteratorInitialized, buffer: .init(), failure: failure, onTermination: initial.onTermination ) ) return .callOnTermination(initial.onTermination) case .streaming(let streaming): if let consumerContinuation = streaming.consumerContinuation { // We have a continuation, this means our buffer must be empty // Furthermore, we can now transition to finished // and resume the continuation with the failure precondition(streaming.buffer.isEmpty, "Expected an empty buffer") precondition( streaming.producerContinuations.isEmpty, "Expected no suspended producers" ) self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) return .resumeConsumerAndCallOnTermination( consumerContinuation: consumerContinuation, failure: failure, onTermination: streaming.onTermination ) } else { self._state = .sourceFinished( .init( iteratorInitialized: streaming.iteratorInitialized, buffer: streaming.buffer, failure: failure, onTermination: streaming.onTermination ) ) return .resumeProducers( producerContinuations: Array(streaming.producerContinuations.map { $0.1 }) ) } case .sourceFinished, .finished: // If the source has finished, finishing again has no effect. return .none case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `next()`. @usableFromInline enum NextAction { /// Indicates that the element should be returned to the caller. case returnElement(Element) /// Indicates that the element should be returned to the caller and that all producers should be called. case returnElementAndResumeProducers(Element, [(Result) -> Void]) /// Indicates that the `Error` should be returned to the caller and that `onTermination` should be called. case returnErrorAndCallOnTermination((any Error)?, (() -> Void)?) /// Indicates that the `nil` should be returned to the caller. case returnNil /// Indicates that the `Task` of the caller should be suspended. case suspendTask } @inlinable mutating func next() -> NextAction { switch self._state { case .initial(let initial): // We are not interacting with the back-pressure strategy here because // we are doing this inside `next(:)` self._state = .streaming( .init( backPressureStrategy: initial.backPressureStrategy, iteratorInitialized: initial.iteratorInitialized, onTermination: initial.onTermination, buffer: Deque(), consumerContinuation: nil, producerContinuations: .init(), cancelledAsyncProducers: .init(), hasOutstandingDemand: false ) ) return .suspendTask case .streaming(var streaming): guard streaming.consumerContinuation == nil else { // We have multiple AsyncIterators iterating the sequence fatalError("AsyncStream internal inconsistency") } self._state = .modify if let element = streaming.buffer.popFirst() { // We have an element to fulfil the demand right away. streaming.hasOutstandingDemand = streaming.backPressureStrategy.didConsume(element: element) if streaming.hasOutstandingDemand { // There is demand and we have to resume our producers let producers = Array(streaming.producerContinuations.map { $0.1 }) streaming.producerContinuations.removeAll() self._state = .streaming(streaming) return .returnElementAndResumeProducers(element, producers) } else { // We don't have any new demand, so we can just return the element. self._state = .streaming(streaming) return .returnElement(element) } } else { // There is nothing in the buffer to fulfil the demand so we need to suspend. // We are not interacting with the back-pressure strategy here because // we are doing this inside `suspendNext` self._state = .streaming(streaming) return .suspendTask } case .sourceFinished(var sourceFinished): // Check if we have an element left in the buffer and return it self._state = .modify if let element = sourceFinished.buffer.popFirst() { self._state = .sourceFinished(sourceFinished) return .returnElement(element) } else { // We are returning the queued failure now and can transition to finished self._state = .finished(iteratorInitialized: sourceFinished.iteratorInitialized) return .returnErrorAndCallOnTermination( sourceFinished.failure, sourceFinished.onTermination ) } case .finished: return .returnNil case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `suspendNext()`. @usableFromInline enum SuspendNextAction { /// Indicates that the consumer should be resumed. case resumeConsumerWithElement(CheckedContinuation, Element) /// Indicates that the consumer and all producers should be resumed. case resumeConsumerWithElementAndProducers( CheckedContinuation, Element, [(Result) -> Void] ) /// Indicates that the consumer should be resumed with the failure and that `onTermination` should be called. case resumeConsumerWithErrorAndCallOnTermination( CheckedContinuation, (any Error)?, (() -> Void)? ) /// Indicates that the consumer should be resumed with `nil`. case resumeConsumerWithNil(CheckedContinuation) } @inlinable mutating func suspendNext( continuation: CheckedContinuation ) -> SuspendNextAction? { switch self._state { case .initial: // We need to transition to streaming before we can suspend preconditionFailure("AsyncStream internal inconsistency") case .streaming(var streaming): guard streaming.consumerContinuation == nil else { // We have multiple AsyncIterators iterating the sequence fatalError( "This should never happen since we only allow a single Iterator to be created" ) } self._state = .modify // We have to check here again since we might have a producer interleave next and suspendNext if let element = streaming.buffer.popFirst() { // We have an element to fulfil the demand right away. streaming.hasOutstandingDemand = streaming.backPressureStrategy.didConsume(element: element) if streaming.hasOutstandingDemand { // There is demand and we have to resume our producers let producers = Array(streaming.producerContinuations.map { $0.1 }) streaming.producerContinuations.removeAll() self._state = .streaming(streaming) return .resumeConsumerWithElementAndProducers( continuation, element, producers ) } else { // We don't have any new demand, so we can just return the element. self._state = .streaming(streaming) return .resumeConsumerWithElement(continuation, element) } } else { // There is nothing in the buffer to fulfil the demand so we to store the continuation. streaming.consumerContinuation = continuation self._state = .streaming(streaming) return .none } case .sourceFinished(var sourceFinished): // Check if we have an element left in the buffer and return it self._state = .modify if let element = sourceFinished.buffer.popFirst() { self._state = .sourceFinished(sourceFinished) return .resumeConsumerWithElement(continuation, element) } else { // We are returning the queued failure now and can transition to finished self._state = .finished(iteratorInitialized: sourceFinished.iteratorInitialized) return .resumeConsumerWithErrorAndCallOnTermination( continuation, sourceFinished.failure, sourceFinished.onTermination ) } case .finished: return .resumeConsumerWithNil(continuation) case .modify: fatalError("AsyncStream internal inconsistency") } } /// Actions returned by `cancelNext()`. @usableFromInline enum CancelNextAction { /// Indicates that the continuation should be resumed with a cancellation error, the producers should be finished and call onTermination. case resumeConsumerWithCancellationErrorAndCallOnTermination( CheckedContinuation, (() -> Void)? ) /// Indicates that the producers should be finished and call onTermination. case failProducersAndCallOnTermination([(Result) -> Void], (() -> Void)?) } @inlinable mutating func cancelNext() -> CancelNextAction? { switch self._state { case .initial: // We need to transition to streaming before we can suspend fatalError("AsyncStream internal inconsistency") case .streaming(let streaming): self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) if let consumerContinuation = streaming.consumerContinuation { precondition( streaming.producerContinuations.isEmpty, "Internal inconsistency. Unexpected producer continuations." ) return .resumeConsumerWithCancellationErrorAndCallOnTermination( consumerContinuation, streaming.onTermination ) } else { return .failProducersAndCallOnTermination( Array(streaming.producerContinuations.map { $0.1 }), streaming.onTermination ) } case .sourceFinished, .finished: return .none case .modify: fatalError("AsyncStream internal inconsistency") } } } } ================================================ FILE: Sources/OpenAPIURLSession/BufferedStream/Lock.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// // swift-format-ignore-file //===----------------------------------------------------------------------===// // // This source file is part of the SwiftNIO open source project // // Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftNIO project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// #if canImport(Darwin) import Darwin #elseif os(Windows) import ucrt import WinSDK #elseif canImport(Glibc) @preconcurrency import Glibc #elseif canImport(Musl) @preconcurrency import Musl #elseif canImport(Bionic) @preconcurrency import Bionic #elseif canImport(WASILibc) @preconcurrency import WASILibc #if canImport(wasi_pthread) import wasi_pthread #endif #else #error("The concurrency Lock module was unable to identify your C library.") #endif #if os(Windows) @usableFromInline typealias LockPrimitive = SRWLOCK #else @usableFromInline typealias LockPrimitive = pthread_mutex_t #endif /// A utility function that runs the body code only in debug builds, without /// emitting compiler warnings. /// /// This is currently the only way to do this in Swift: see /// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. @inlinable internal func debugOnly(_ body: () -> Void) { assert({ body(); return true }()) } @usableFromInline enum LockOperations: Sendable {} extension LockOperations { @inlinable static func create(_ mutex: UnsafeMutablePointer) { mutex.assertValidAlignment() #if os(Windows) InitializeSRWLock(mutex) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) var attr = pthread_mutexattr_t() pthread_mutexattr_init(&attr) debugOnly { pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) } let err = pthread_mutex_init(mutex, &attr) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } @inlinable static func destroy(_ mutex: UnsafeMutablePointer) { mutex.assertValidAlignment() #if os(Windows) // SRWLOCK does not need to be free'd #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) let err = pthread_mutex_destroy(mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } @inlinable static func lock(_ mutex: UnsafeMutablePointer) { mutex.assertValidAlignment() #if os(Windows) AcquireSRWLockExclusive(mutex) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) let err = pthread_mutex_lock(mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } @inlinable static func unlock(_ mutex: UnsafeMutablePointer) { mutex.assertValidAlignment() #if os(Windows) ReleaseSRWLockExclusive(mutex) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) let err = pthread_mutex_unlock(mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } } // Tail allocate both the mutex and a generic value using ManagedBuffer. // Both the header pointer and the elements pointer are stable for // the class's entire lifetime. // // However, for safety reasons, we elect to place the lock in the "elements" // section of the buffer instead of the head. The reasoning here is subtle, // so buckle in. // // _As a practical matter_, the implementation of ManagedBuffer ensures that // the pointer to the header is stable across the lifetime of the class, and so // each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` // the value of the header pointer will be the same. This is because ManagedBuffer uses // `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure // that it does not invoke any weird Swift accessors that might copy the value. // // _However_, the header is also available via the `.header` field on the ManagedBuffer. // This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends // do not interact with Swift's exclusivity model. That is, the various `with` functions do not // conceptually trigger a mutating access to `.header`. For elements this isn't a concern because // there's literally no other way to perform the access, but for `.header` it's entirely possible // to accidentally recursively read it. // // Our implementation is free from these issues, so we don't _really_ need to worry about it. // However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive // in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, // and future maintainers will be happier that we were cautious. // // See also: https://github.com/apple/swift/pull/40000 @usableFromInline final class LockStorage: ManagedBuffer { @inlinable static func create(value: Value) -> Self { let buffer = Self.create(minimumCapacity: 1) { _ in value } // Intentionally using a force cast here to avoid a miss compiliation in 5.10. // This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer // can eliminate the upcast/downcast pair let storage = buffer as! Self storage.withUnsafeMutablePointers { _, lockPtr in LockOperations.create(lockPtr) } return storage } @inlinable func lock() { self.withUnsafeMutablePointerToElements { lockPtr in LockOperations.lock(lockPtr) } } @inlinable func unlock() { self.withUnsafeMutablePointerToElements { lockPtr in LockOperations.unlock(lockPtr) } } @inlinable deinit { self.withUnsafeMutablePointerToElements { lockPtr in LockOperations.destroy(lockPtr) } } @inlinable func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { try self.withUnsafeMutablePointerToElements { lockPtr in try body(lockPtr) } } @inlinable func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { try self.withUnsafeMutablePointers { valuePtr, lockPtr in LockOperations.lock(lockPtr) defer { LockOperations.unlock(lockPtr) } return try mutate(&valuePtr.pointee) } } } // This compiler guard is here becaue `ManagedBuffer` is already declaring // Sendable unavailability after 6.1, which `LockStorage` inherits. #if compiler(<6.2) @available(*, unavailable) extension LockStorage: Sendable {} #endif /// A threading lock based on `libpthread` instead of `libdispatch`. /// /// - Note: ``Lock`` has reference semantics. /// /// This object provides a lock on top of a single `pthread_mutex_t`. This kind /// of lock is safe to use with `libpthread`-based threading models, such as the /// one used by NIO. On Windows, the lock is based on the substantially similar /// `SRWLOCK` type. @usableFromInline struct Lock { @usableFromInline internal let _storage: LockStorage /// Create a new lock. @inlinable init() { self._storage = .create(value: ()) } /// Acquire the lock. /// /// Whenever possible, consider using `withLock` instead of this method and /// `unlock`, to simplify lock handling. @inlinable func lock() { self._storage.lock() } /// Release the lock. /// /// Whenever possible, consider using `withLock` instead of this method and /// `lock`, to simplify lock handling. @inlinable func unlock() { self._storage.unlock() } @inlinable internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { try self._storage.withLockPrimitive(body) } } extension Lock { /// Acquire the lock for the duration of the given block. /// /// This convenience method should be preferred to `lock` and `unlock` in /// most situations, as it ensures that the lock will be released regardless /// of how `body` exits. /// /// - Parameter body: The block to execute while holding the lock. /// - Returns: The value returned by the block. @inlinable func withLock(_ body: () throws -> T) rethrows -> T { self.lock() defer { self.unlock() } return try body() } @inlinable func withLockVoid(_ body: () throws -> Void) rethrows { try self.withLock(body) } } extension Lock: @unchecked Sendable {} extension UnsafeMutablePointer { @inlinable func assertValidAlignment() { assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) } } /// Provides locked access to `Value`. /// /// - Note: ``LockedValueBox`` has reference semantics and holds the `Value` /// alongside a lock behind a reference. /// /// This is no different than creating a ``Lock`` and protecting all /// accesses to a value using the lock. But it's easy to forget to actually /// acquire/release the lock in the correct place. ``LockedValueBox`` makes /// that much easier. @usableFromInline struct LockedValueBox { @usableFromInline internal let _storage: LockStorage /// Initialize the `Value`. @inlinable init(_ value: Value) { self._storage = .create(value: value) } /// Access the `Value`, allowing mutation of it. @inlinable func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { try self._storage.withLockedValue(mutate) } /// Provides an unsafe view over the lock and its value. /// /// This can be beneficial when you require fine grained control over the lock in some /// situations but don't want lose the benefits of ``withLockedValue(_:)`` in others by /// switching to ``NIOLock``. var unsafe: Unsafe { Unsafe(_storage: self._storage) } /// Provides an unsafe view over the lock and its value. struct Unsafe { @usableFromInline let _storage: LockStorage /// Manually acquire the lock. @inlinable func lock() { self._storage.lock() } /// Manually release the lock. @inlinable func unlock() { self._storage.unlock() } /// Mutate the value, assuming the lock has been acquired manually. /// /// - Parameter mutate: A closure with scoped access to the value. /// - Returns: The result of the `mutate` closure. @inlinable func withValueAssumingLockIsAcquired( _ mutate: (_ value: inout Value) throws -> Result ) rethrows -> Result { try self._storage.withUnsafeMutablePointerToHeader { value in try mutate(&value.pointee) } } } } extension LockedValueBox: @unchecked Sendable where Value: Sendable {} extension LockedValueBox.Unsafe: @unchecked Sendable where Value: Sendable {} ================================================ FILE: Sources/OpenAPIURLSession/Documentation.docc/Documentation.md ================================================ # ``OpenAPIURLSession`` Send HTTP requests to the server using URLSession from the Foundation framework. ## Overview A client transport that uses the [URLSession](https://developer.apple.com/documentation/foundation/urlsession) type from the [Foundation](https://developer.apple.com/documentation/foundation) framework to perform HTTP operations. Use the transport with client code generated by [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator). ### Supported platforms and minimum versions | macOS | Linux | iOS | tvOS | watchOS | visionOS | | :-: | :-: | :-: | :-: | :-: | :-: | | ✅ 10.15+ | ✅ | ✅ 13+ | ✅ 13+ | ✅ 6+ | ✅ 1+ | Note: Streaming support only available on macOS 12+, iOS 15+, tvOS 15+, watchOS 8+, and visionOS 1+. For streaming support on Linux, please use the [AsyncHTTPClient Transport](https://github.com/swift-server/swift-openapi-async-http-client) ### Usage Add the package dependency in your `Package.swift`: ```swift .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"), ``` Next, in your target, add `OpenAPIURLSession` to your dependencies: ```swift .target(name: "MyTarget", dependencies: [ .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), ]), ``` Then, to get started, check out ``URLSessionTransport``. ## Topics ### Essentials - ``URLSessionTransport`` ================================================ FILE: Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/BidirectionalStreamingURLSessionDelegate.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import OpenAPIRuntime import HTTPTypes #if canImport(Darwin) import Foundation /// Delegate that supports bidirectional streaming of request and response bodies. /// /// While URLSession provides a high-level API that returns an async sequence of /// bytes, `bytes(for:delegate:)`, but does not provide an API that takes an async sequence /// as a request body. For instance, `upload(for:delegate:)` and `upload(fromFile:delegate:)` /// both buffer the entire response body and return `Data`. /// /// Additionally, bridging `URLSession.AsyncBytes`, which is an `AsyncSequence` to /// `OpenAPIRuntime.HTTPBody`, an `AsyncSequence`, is problematic and will /// incur an allocation for every byte. /// /// This delegate vends the response body as a `HTTBody` with one chunk for each /// `urlSession(_:didReceive data:)` callback. It also provides backpressure, which will /// suspend and resume the URLSession task based on a configurable high and low watermark. /// /// When performing requests without a body, this delegate should be used with a /// `URLSessionDataTask` to stream the response body. /// /// When performing requests with a body, this delegate should be used with a /// `URLSessionUploadTask` using `uploadTask(withStreamedRequest:delegate:)`, which will /// ask the delegate for a `InputStream` for the request body via the /// `urlSession(_:needNewBodyStreamForTask:)` callback. /// /// The `urlSession(_:needNewBodyStreamForTask:)` callback will create a pair of bound /// streams, bridge the `HTTPBody` request body to the `OutputStream` and return the /// `InputStream` to URLSession. Backpressure for the request body stream is provided /// as an implementation detail of how URLSession reads from the `InputStream`. /// /// Note that `urlSession(_:needNewBodyStreamForTask:)` may be called more than once, e.g. /// when performing a HTTP redirect, upon which the delegate is expected to create a new /// `InputStream` for the request body. This is only possible if the underlying `HTTPBody` /// request body can be iterated multiple times, i.e. `iterationBehavior == .multiple`. /// If the request body cannot be iterated multiple times, then the URLSession task will be cancelled. final class BidirectionalStreamingURLSessionDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate { let requestBody: HTTPBody? var hasAlreadyIteratedRequestBody: Bool /// In addition to the callback lock, there is one point of rentrancy, where the response stream callback gets fired /// immediately, for this we have a different lock, which protects `hasSuspendedURLSessionTask`. var hasSuspendedURLSessionTask: LockedValueBox let requestStreamBufferSize: Int var requestStream: HTTPBodyOutputStreamBridge? typealias ResponseContinuation = CheckedContinuation var responseContinuation: ResponseContinuation? typealias ResponseBodyStream = BufferedStream var responseBodyStream: ResponseBodyStream var responseBodyStreamSource: ResponseBodyStream.Source /// This lock is taken for the duration of all delegate callbacks to protect the mutable delegate state. /// /// Although all the delegate callbacks are performed on the session's `delegateQueue`, there is no guarantee that /// this is a _serial_ queue. /// /// Regardless of the type of delegate queue, URLSession will attempt to order the callbacks for each task in a /// sensible way, but it cannot be guaranteed, specifically when the URLSession task is cancelled. /// /// Therefore, even though the `suspend()`, `resume()`, and `cancel()` URLSession methods are thread-safe, we need /// to protect any mutable state within the delegate itself. let callbackLock = Lock() /// Use `bidirectionalStreamingRequest(for:baseURL:requestBody:requestStreamBufferSize:responseStreamWatermarks:)`. init(requestBody: HTTPBody?, requestStreamBufferSize: Int, responseStreamWatermarks: (low: Int, high: Int)) { self.requestBody = requestBody self.hasAlreadyIteratedRequestBody = false self.hasSuspendedURLSessionTask = LockedValueBox(false) self.requestStreamBufferSize = requestStreamBufferSize (self.responseBodyStream, self.responseBodyStreamSource) = ResponseBodyStream.makeStream( backPressureStrategy: .customWatermark( low: responseStreamWatermarks.low, high: responseStreamWatermarks.high, waterLevelForElement: { $0.count } ) ) } func urlSession(_ session: URLSession, needNewBodyStreamForTask task: URLSessionTask) async -> InputStream? { callbackLock.withLock { debug("Task delegate: needNewBodyStreamForTask") // If the HTTP body cannot be iterated multiple times then bad luck; the only thing // we can do is cancel the task and return nil. if hasAlreadyIteratedRequestBody { guard requestBody!.iterationBehavior == .multiple else { debug("Task delegate: Cannot rewind request body, cancelling task") task.cancel() return nil } } hasAlreadyIteratedRequestBody = true // Create a fresh pair of streams. let (inputStream, outputStream) = createStreamPair(withBufferSize: requestStreamBufferSize) // Bridge the output stream to the request body (which opens the output stream). requestStream = HTTPBodyOutputStreamBridge(outputStream, requestBody!) // Return the new input stream (unopened, it gets opened by URLSession). return inputStream } } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { callbackLock.withLock { debug("Task delegate: didReceive data (numBytes: \(data.count))") do { switch try responseBodyStreamSource.write(contentsOf: CollectionOfOne(ArraySlice(data))) { case .produceMore: break case .enqueueCallback(let callbackToken): let shouldActuallyEnqueueCallback = hasSuspendedURLSessionTask.withLockedValue { hasSuspendedURLSessionTask in if hasSuspendedURLSessionTask { debug("Task delegate: already suspended task, not enqueing another writer callback") return false } debug("Task delegate: response stream backpressure, suspending task and enqueing callback") dataTask.suspend() hasSuspendedURLSessionTask = true return true } if shouldActuallyEnqueueCallback { responseBodyStreamSource.enqueueCallback(callbackToken: callbackToken) { result in self.hasSuspendedURLSessionTask.withLockedValue { hasSuspendedURLSessionTask in switch result { case .success: debug("Task delegate: response stream callback, resuming task") dataTask.resume() hasSuspendedURLSessionTask = false case .failure(let error): debug("Task delegate: response stream callback, cancelling task, error: \(error)") dataTask.cancel() } } } } } } catch { debug("Task delegate: response stream consumer terminated, cancelling task") dataTask.cancel() } } } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition { callbackLock.withLock { debug("Task delegate: didReceive response") responseContinuation?.resume(returning: response) responseContinuation = nil return .allow } } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { callbackLock.withLock { debug("Task delegate: didCompleteWithError (error: \(String(describing: error)))") responseBodyStreamSource.finish(throwing: error) if let error { responseContinuation?.resume(throwing: error) responseContinuation = nil } } } } extension BidirectionalStreamingURLSessionDelegate: @unchecked Sendable {} // State synchronized using DispatchQueue. private func createStreamPair(withBufferSize bufferSize: Int) -> (InputStream, OutputStream) { var inputStream: InputStream? var outputStream: OutputStream? Stream.getBoundStreams(withBufferSize: bufferSize, inputStream: &inputStream, outputStream: &outputStream) guard let inputStream, let outputStream else { fatalError("getBoundStreams did not return non-nil streams") } return (inputStream, outputStream) } #endif // canImport(Darwin) ================================================ FILE: Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/HTTPBodyOutputStreamBridge.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import OpenAPIRuntime import HTTPTypes #if canImport(Darwin) import Foundation final class HTTPBodyOutputStreamBridge: NSObject, StreamDelegate { static let streamQueue = DispatchQueue(label: "HTTPBodyStreamDelegate", autoreleaseFrequency: .workItem) let httpBody: HTTPBody let outputStream: OutputStream private(set) var state: State { didSet { debug("Output stream delegate state transition: \(oldValue) -> \(state)") } } /// Creates a new `HTTPBodyOutputStreamBridge` and opens the output stream. init(_ outputStream: OutputStream, _ httpBody: HTTPBody) { self.httpBody = httpBody self.outputStream = outputStream self.state = .initial super.init() self.outputStream.delegate = self CFWriteStreamSetDispatchQueue(self.outputStream as CFWriteStream, Self.streamQueue) self.outputStream.open() } deinit { debug("Output stream delegate deinit") outputStream.delegate = nil } func performAction(_ action: State.Action) { debug("Output stream delegate performing action from state machine: \(action)") dispatchPrecondition(condition: .onQueue(Self.streamQueue)) switch action { case .none: return case .resumeProducer(let producerContinuation): producerContinuation.resume() performAction(state.resumedProducer()) case .writeBytes(let chunk): writePendingBytes(chunk) case .cancelProducerAndCloseStream(let producerContinuation): producerContinuation.resume(throwing: CancellationError()) outputStream.close() case .cancelProducer(let producerContinuation): producerContinuation.resume(throwing: CancellationError()) case .closeStream: outputStream.close() } } func startWriterTask() { dispatchPrecondition(condition: .onQueue(Self.streamQueue)) let task = Task { dispatchPrecondition(condition: .notOnQueue(Self.streamQueue)) for try await chunk in httpBody { try await withCheckedThrowingContinuation { continuation in Self.streamQueue.async { debug("Output stream delegate produced chunk and suspended producer.") self.performAction(self.state.producedChunkAndSuspendedProducer(chunk, continuation)) } } } Self.streamQueue.async { debug("Output stream delegate wrote final chunk.") self.performAction(self.state.wroteFinalChunk()) } } performAction(state.startedProducerTask(task)) } private func writePendingBytes(_ bytesToWrite: Chunk) { dispatchPrecondition(condition: .onQueue(Self.streamQueue)) precondition(!bytesToWrite.isEmpty, "\(#function) must be called with non-empty bytes") guard outputStream.streamStatus == .open else { debug("Output stream closed unexpectedly.") performAction(state.wroteBytes(numBytesWritten: 0, streamStillHasSpaceAvailable: false)) return } switch bytesToWrite.withUnsafeBytes({ outputStream.write($0.baseAddress!, maxLength: bytesToWrite.count) }) { case 0: debug("Output stream delegate reached end of stream when writing.") performAction(state.endEncountered()) case -1: debug("Output stream delegate encountered error writing to stream: \(outputStream.streamError!).") performAction(state.errorOccurred(outputStream.streamError!)) case let written where written > 0: debug("Output stream delegate wrote \(written) bytes to stream.") performAction( state.wroteBytes(numBytesWritten: written, streamStillHasSpaceAvailable: outputStream.hasSpaceAvailable) ) default: preconditionFailure("OutputStream.write(_:maxLength:) returned undocumented value") } } func stream(_ stream: Stream, handle event: Stream.Event) { dispatchPrecondition(condition: .onQueue(Self.streamQueue)) debug("Output stream delegate received event: \(event).") switch event { case .openCompleted: guard case .initial = state else { debug("Output stream delegate ignoring duplicate openCompleted event.") return } startWriterTask() case .hasSpaceAvailable: performAction(state.spaceBecameAvailable()) case .errorOccurred: performAction(state.errorOccurred(stream.streamError!)) case .endEncountered: performAction(state.endEncountered()) default: debug("Output stream ignoring event: \(event).") break } } } extension HTTPBodyOutputStreamBridge { typealias Chunk = ArraySlice typealias ProducerTask = Task typealias ProducerContinuation = CheckedContinuation enum State { case initial case waitingForBytes(spaceAvailable: Bool) case haveBytes(spaceAvailable: Bool, Chunk, ProducerContinuation) case needBytes(spaceAvailable: Bool, ProducerContinuation) case closed((any Error)?) mutating func startedProducerTask(_ producerTask: ProducerTask) -> Action { switch self { case .initial: self = .waitingForBytes(spaceAvailable: false) return .none case .waitingForBytes, .haveBytes, .needBytes, .closed: preconditionFailure("\(#function) called in invalid state: \(self)") } } mutating func producedChunkAndSuspendedProducer(_ chunk: Chunk, _ producerContinuation: ProducerContinuation) -> Action { switch self { case .waitingForBytes(let spaceAvailable): self = .haveBytes(spaceAvailable: spaceAvailable, chunk, producerContinuation) guard spaceAvailable else { return .none } return .writeBytes(chunk) case .closed: return .cancelProducer(producerContinuation) case .initial, .haveBytes, .needBytes: preconditionFailure("\(#function) called in invalid state: \(self)") } } mutating func wroteBytes(numBytesWritten: Int, streamStillHasSpaceAvailable: Bool) -> Action { switch self { case .haveBytes(let spaceAvailable, let chunk, let producerContinuation): guard spaceAvailable, numBytesWritten <= chunk.count else { preconditionFailure() } let remaining = chunk.dropFirst(numBytesWritten) guard remaining.isEmpty else { self = .haveBytes(spaceAvailable: streamStillHasSpaceAvailable, remaining, producerContinuation) guard streamStillHasSpaceAvailable else { return .none } return .writeBytes(remaining) } self = .needBytes(spaceAvailable: streamStillHasSpaceAvailable, producerContinuation) return .resumeProducer(producerContinuation) case .initial, .needBytes, .waitingForBytes, .closed: preconditionFailure("\(#function) called in invalid state: \(self)") } } mutating func resumedProducer() -> Action { switch self { case .needBytes(let spaceAvailable, _): self = .waitingForBytes(spaceAvailable: spaceAvailable) return .none case .initial, .haveBytes, .waitingForBytes, .closed: preconditionFailure("\(#function) called in invalid state: \(self)") } } mutating func errorOccurred(_ error: any Error) -> Action { switch self { case .initial: self = .closed(error) return .none case .waitingForBytes(_): self = .closed(error) return .closeStream case .haveBytes(_, _, let producerContinuation): self = .closed(error) return .cancelProducerAndCloseStream(producerContinuation) case .needBytes(_, let producerContinuation): self = .closed(error) return .cancelProducerAndCloseStream(producerContinuation) case .closed: preconditionFailure("\(#function) called in invalid state: \(self)") } } mutating func wroteFinalChunk() -> Action { switch self { case .waitingForBytes(_): self = .closed(nil) return .closeStream case .initial, .haveBytes, .needBytes, .closed: preconditionFailure("\(#function) called in invalid state: \(self)") } } mutating func endEncountered() -> Action { switch self { case .waitingForBytes(_): self = .closed(nil) return .closeStream case .haveBytes(_, _, let producerContinuation): self = .closed(nil) return .cancelProducerAndCloseStream(producerContinuation) case .needBytes(_, let producerContinuation): self = .closed(nil) return .cancelProducerAndCloseStream(producerContinuation) case .initial, .closed: preconditionFailure("\(#function) called in invalid state: \(self)") } } mutating func spaceBecameAvailable() -> Action { switch self { case .waitingForBytes(_): self = .waitingForBytes(spaceAvailable: true) return .none case .haveBytes(_, let chunk, let producerContinuation): self = .haveBytes(spaceAvailable: true, chunk, producerContinuation) return .writeBytes(chunk) case .needBytes(_, let producerContinuation): self = .needBytes(spaceAvailable: true, producerContinuation) return .none case .closed: debug("Ignoring space available event in closed state") return .none case .initial: preconditionFailure("\(#function) called in invalid state: \(self)") } } enum Action { case none case resumeProducer(ProducerContinuation) case writeBytes(Chunk) case cancelProducerAndCloseStream(ProducerContinuation) case cancelProducer(ProducerContinuation) case closeStream } } } extension HTTPBodyOutputStreamBridge: @unchecked Sendable {} // State synchronized using DispatchQueue. extension HTTPBodyOutputStreamBridge.State: CustomStringConvertible { var description: String { switch self { case .initial: return "initial" case .waitingForBytes(let spaceAvailable): return "waitingForBytes(spaceAvailable: \(spaceAvailable))" case .haveBytes(let spaceAvailable, let chunk, _): return "haveBytes(spaceAvailable: \(spaceAvailable), [\(chunk.count) bytes])" case .needBytes(let spaceAvailable, _): return "needBytes (spaceAvailable: \(spaceAvailable), _)" case .closed(let error): return "closed (error: \(String(describing: error)))" } } } extension HTTPBodyOutputStreamBridge.State.Action: CustomStringConvertible { var description: String { switch self { case .none: return "none" case .resumeProducer: return "resumeProducer" case .writeBytes: return "writeBytes" case .cancelProducerAndCloseStream: return "cancelProducerAndCloseStream" case .cancelProducer: return "cancelProducer" case .closeStream: return "closeStream" } } } #endif // canImport(Darwin) ================================================ FILE: Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/URLSession+Extensions.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import OpenAPIRuntime import HTTPTypes #if canImport(Darwin) import Foundation @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension URLSession { func bidirectionalStreamingRequest( for request: HTTPRequest, baseURL: URL, requestBody: HTTPBody?, requestStreamBufferSize: Int, responseStreamWatermarks: (low: Int, high: Int) ) async throws -> (HTTPResponse, HTTPBody?) { let urlRequest = try URLRequest(request, baseURL: baseURL) let task: URLSessionTask if requestBody != nil { task = uploadTask(withStreamedRequest: urlRequest) } else { task = dataTask(with: urlRequest) } return try await withTaskCancellationHandler { try Task.checkCancellation() let delegate = BidirectionalStreamingURLSessionDelegate( requestBody: requestBody, requestStreamBufferSize: requestStreamBufferSize, responseStreamWatermarks: responseStreamWatermarks ) let response = try await withCheckedThrowingContinuation { continuation in delegate.responseContinuation = continuation task.delegate = delegate task.resume() } let responseBody = HTTPBody( delegate.responseBodyStream, length: .init(from: response), iterationBehavior: .single ) try Task.checkCancellation() return (try HTTPResponse(response), responseBody) } onCancel: { debug("Concurrency task cancelled, cancelling URLSession task.") task.cancel() } } } #endif // canImport(Darwin) ================================================ FILE: Sources/OpenAPIURLSession/URLSessionTransport.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import OpenAPIRuntime import HTTPTypes #if canImport(Darwin) import Foundation #else @preconcurrency import struct Foundation.URL import struct Foundation.URLComponents import struct Foundation.Data import protocol Foundation.LocalizedError import class Foundation.FileHandle #if canImport(FoundationNetworking) @preconcurrency import struct FoundationNetworking.URLRequest import class FoundationNetworking.URLSession import class FoundationNetworking.URLSessionTask import class FoundationNetworking.URLResponse import class FoundationNetworking.HTTPURLResponse #endif #endif /// A client transport that performs HTTP operations using the URLSession type /// provided by the Foundation framework. /// /// ### Use the URLSession transport /// /// Instantiate the transport: /// /// let transport = URLSessionTransport() /// /// Instantiate the `Client` type generated by the Swift OpenAPI Generator for /// your provided OpenAPI document. For example: /// /// let client = Client( /// serverURL: URL(string: "https://example.com")!, /// transport: transport /// ) /// /// Use the client to make HTTP calls defined in your OpenAPI document. For /// example, if the OpenAPI document contains an HTTP operation with /// the identifier `checkHealth`, call it from Swift with: /// /// let response = try await client.checkHealth() /// /// ### Provide a custom URLSession /// /// The ``URLSessionTransport/Configuration-swift.struct`` type allows you to /// provide a custom URLSession and tweak behaviors such as the default /// timeouts, authentication challenges, and more. public struct URLSessionTransport: ClientTransport { /// A set of configuration values for the URLSession transport. public struct Configuration: Sendable { /// The URLSession used for performing HTTP operations. public var session: URLSession /// Creates a new configuration with the provided session. /// - Parameters: /// - session: The URLSession used for performing HTTP operations. /// If none is provided, the system uses the shared URLSession. /// - httpBodyProcessingMode: The mode used to process HTTP request and response bodies. public init(session: URLSession = .shared, httpBodyProcessingMode: HTTPBodyProcessingMode = .platformDefault) { let implementation = httpBodyProcessingMode.implementation self.init(session: session, implementation: implementation) } /// Creates a new configuration with the provided session. /// - Parameter session: The URLSession used for performing HTTP operations. /// If none is provided, the system uses the shared URLSession. public init(session: URLSession = .shared) { self.init(session: session, implementation: .platformDefault) } /// Specifies the mode in which HTTP request and response bodies are processed. public struct HTTPBodyProcessingMode: Sendable { /// Exposing the internal implementation directly. fileprivate let implementation: Configuration.Implementation private init(_ implementation: Configuration.Implementation) { self.implementation = implementation } /// Use this mode to force URLSessionTransport to transfer data in a buffered mode, even if /// streaming would be available on the platform. public static let buffered = HTTPBodyProcessingMode(.buffering) /// Data is transfered via streaming if available on the platform, else it falls back to buffering. public static let platformDefault = HTTPBodyProcessingMode(.platformDefault) } enum Implementation { case buffering case streaming(requestBodyStreamBufferSize: Int, responseBodyStreamWatermarks: (low: Int, high: Int)) } var implementation: Implementation init(session: URLSession = .shared, implementation: Implementation = .platformDefault) { self.session = session if case .streaming = implementation { precondition(Implementation.platformSupportsStreaming, "Streaming not supported on platform") } self.implementation = implementation } } /// A set of configuration values used by the transport. public var configuration: Configuration /// Creates a new URLSession-based transport. /// - Parameter configuration: A set of configuration values used by the transport. public init(configuration: Configuration = .init()) { self.configuration = configuration } /// Sends the underlying HTTP request and returns the received HTTP response. /// - Parameters: /// - request: An HTTP request. /// - requestBody: An HTTP request body. /// - baseURL: A server base URL. /// - operationID: The identifier of the OpenAPI operation. /// - Returns: An HTTP response and its body. /// - Throws: If there was an error performing the HTTP request. public func send(_ request: HTTPRequest, body requestBody: HTTPBody?, baseURL: URL, operationID: String) async throws -> (HTTPResponse, HTTPBody?) { switch configuration.implementation { case .streaming(let requestBodyStreamBufferSize, let responseBodyStreamWatermarks): #if canImport(Darwin) guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { throw URLSessionTransportError.streamingNotSupported } return try await configuration.session.bidirectionalStreamingRequest( for: request, baseURL: baseURL, requestBody: requestBody, requestStreamBufferSize: requestBodyStreamBufferSize, responseStreamWatermarks: responseBodyStreamWatermarks ) #else throw URLSessionTransportError.streamingNotSupported #endif case .buffering: return try await configuration.session.bufferedRequest( for: request, baseURL: baseURL, requestBody: requestBody ) } } } extension HTTPBody.Length { init(from urlResponse: URLResponse) { if urlResponse.expectedContentLength == -1 { self = .unknown } else { self = .known(urlResponse.expectedContentLength) } } } /// Specialized error thrown by the transport. internal enum URLSessionTransportError: Error { /// Invalid URL composed from base URL and received request. case invalidRequestURL(path: String, method: HTTPRequest.Method, baseURL: URL) /// Returned `URLResponse` could not be converted to `HTTPURLResponse`. case notHTTPResponse(URLResponse) /// Returned `HTTPURLResponse` has an invalid status code case invalidResponseStatusCode(HTTPURLResponse) /// Returned `URLResponse` was nil case noResponse(url: URL?) /// Platform does not support streaming. case streamingNotSupported } extension HTTPResponse { init(_ urlResponse: URLResponse) throws { guard let httpResponse = urlResponse as? HTTPURLResponse else { throw URLSessionTransportError.notHTTPResponse(urlResponse) } guard (0...999).contains(httpResponse.statusCode) else { throw URLSessionTransportError.invalidResponseStatusCode(httpResponse) } self.init(status: .init(code: httpResponse.statusCode)) if let fields = httpResponse.allHeaderFields as? [String: String] { self.headerFields.reserveCapacity(fields.count) for (name, value) in fields { if let name = HTTPField.Name(name) { self.headerFields.append(HTTPField(name: name, isoLatin1Value: value)) } } } } } extension URLRequest { init(_ request: HTTPRequest, baseURL: URL) throws { guard var baseUrlComponents = URLComponents(string: baseURL.absoluteString), let requestUrlComponents = URLComponents(string: request.path ?? "") else { throw URLSessionTransportError.invalidRequestURL( path: request.path ?? "", method: request.method, baseURL: baseURL ) } let path = requestUrlComponents.percentEncodedPath baseUrlComponents.percentEncodedPath += path baseUrlComponents.percentEncodedQuery = requestUrlComponents.percentEncodedQuery guard let url = baseUrlComponents.url else { throw URLSessionTransportError.invalidRequestURL(path: path, method: request.method, baseURL: baseURL) } self.init(url: url) self.httpMethod = request.method.rawValue var combinedFields = [HTTPField.Name: String](minimumCapacity: request.headerFields.count) for field in request.headerFields { if let existingValue = combinedFields[field.name] { let separator = field.name == .cookie ? "; " : ", " combinedFields[field.name] = "\(existingValue)\(separator)\(field.isoLatin1Value)" } else { combinedFields[field.name] = field.isoLatin1Value } } var headerFields = [String: String](minimumCapacity: combinedFields.count) for (name, value) in combinedFields { headerFields[name.rawName] = value } self.allHTTPHeaderFields = headerFields } } extension String { fileprivate var isASCII: Bool { self.utf8.allSatisfy { $0 & 0x80 == 0 } } } extension HTTPField { fileprivate init(name: Name, isoLatin1Value: String) { if isoLatin1Value.isASCII { self.init(name: name, value: isoLatin1Value) } else { self = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: isoLatin1Value.unicodeScalars.count) { buffer in for (index, scalar) in isoLatin1Value.unicodeScalars.enumerated() { if scalar.value > UInt8.max { buffer[index] = 0x20 } else { buffer[index] = UInt8(truncatingIfNeeded: scalar.value) } } return HTTPField(name: name, value: buffer) } } } fileprivate var isoLatin1Value: String { if self.value.isASCII { return self.value } return self.withUnsafeBytesOfValue { buffer in let scalars = buffer.lazy.map { UnicodeScalar(UInt32($0))! } var string = "" string.unicodeScalars.append(contentsOf: scalars) return string } } } extension URLSessionTransportError: LocalizedError { /// A localized message describing what error occurred. var errorDescription: String? { description } } extension URLSessionTransportError: CustomStringConvertible { /// A textual representation of this instance. var description: String { switch self { case let .invalidRequestURL(path: path, method: method, baseURL: baseURL): return "Invalid request URL from request path: \(path), method: \(method), relative to base URL: \(baseURL.absoluteString)" case .notHTTPResponse(let response): return "Received a non-HTTP response, of type: \(String(describing: type(of: response)))" case .invalidResponseStatusCode(let response): return "Received an HTTP response with invalid status code: \(response.statusCode))" case .noResponse(let url): return "Received a nil response for \(url?.absoluteString ?? "")" case .streamingNotSupported: return "Streaming is not supported on this platform" } } } private let _debugLoggingEnabled = LockedValueBox(false) var debugLoggingEnabled: Bool { get { _debugLoggingEnabled.withLockedValue { $0 } } set { _debugLoggingEnabled.withLockedValue { $0 = newValue } } } private let _standardErrorLock = LockedValueBox(FileHandle.standardError) func debug(_ message: @autoclosure () -> String, function: String = #function, file: String = #file, line: UInt = #line) { assert( { if debugLoggingEnabled { _standardErrorLock.withLockedValue { let logLine = "[\(function) \(file.split(separator: "/").last!):\(line)] \(message())\n" $0.write(Data((logLine).utf8)) } } return true }() ) } extension URLSession { func bufferedRequest(for request: HTTPRequest, baseURL: URL, requestBody: HTTPBody?) async throws -> ( HTTPResponse, HTTPBody? ) { try Task.checkCancellation() var urlRequest = try URLRequest(request, baseURL: baseURL) if let requestBody { urlRequest.httpBody = try await Data(collecting: requestBody, upTo: .max) } try Task.checkCancellation() /// Use `dataTask(with:completionHandler:)` here because `data(for:[delegate:]) async` is only available on /// Darwin platforms newer than our minimum deployment target, and not at all on Linux. let taskBox: LockedValueBox = .init(nil) return try await withTaskCancellationHandler { let (response, maybeResponseBodyData): (URLResponse, Data?) = try await withCheckedThrowingContinuation { continuation in let task = self.dataTask(with: urlRequest) { [urlRequest] data, response, error in if let error { continuation.resume(throwing: error) return } guard let response else { continuation.resume(throwing: URLSessionTransportError.noResponse(url: urlRequest.url)) return } continuation.resume(with: .success((response, data))) } // Swift concurrency task cancelled here. taskBox.withLockedValue { boxedTask in guard task.state == .suspended else { debug("URLSession task cannot be resumed, probably because it was cancelled by onCancel.") return } task.resume() boxedTask = task } } let maybeResponseBody = maybeResponseBodyData.map { data in HTTPBody(data, length: HTTPBody.Length(from: response), iterationBehavior: .multiple) } return (try HTTPResponse(response), maybeResponseBody) } onCancel: { taskBox.withLockedValue { boxedTask in debug("Concurrency task cancelled, cancelling URLSession task.") boxedTask?.cancel() boxedTask = nil } } } } extension URLSessionTransport.Configuration.Implementation { static var platformSupportsStreaming: Bool { #if canImport(Darwin) guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return false } _ = URLSession.bidirectionalStreamingRequest return true #else return false #endif } static var platformDefault: Self { guard platformSupportsStreaming else { return .buffering } return .streaming( requestBodyStreamBufferSize: 16 * 1024, responseBodyStreamWatermarks: (low: 16 * 1024, high: 32 * 1024) ) } } ================================================ FILE: Tests/OpenAPIURLSessionTests/AsyncSyncSequence.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// // swift-format-ignore-file //===----------------------------------------------------------------------===// // // This source file is part of the Swift Async Algorithms open source project // // Copyright (c) 2022 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// extension Sequence { /// An asynchronous sequence containing the same elements as this sequence, /// but on which operations, such as `map` and `filter`, are /// implemented asynchronously. @inlinable var async: AsyncSyncSequence { AsyncSyncSequence(self) } } /// An asynchronous sequence composed from a synchronous sequence. /// /// Asynchronous lazy sequences can be used to interface existing or pre-calculated /// data to interoperate with other asynchronous sequences and algorithms based on /// asynchronous sequences. /// /// This functions similarly to `LazySequence` by accessing elements sequentially /// in the iterator's `next()` method. @frozen public struct AsyncSyncSequence: AsyncSequence { public typealias Element = Base.Element @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline var iterator: Base.Iterator? @usableFromInline init(_ iterator: Base.Iterator) { self.iterator = iterator } @inlinable public mutating func next() async -> Base.Element? { if !Task.isCancelled, let value = iterator?.next() { return value } else { iterator = nil return nil } } } @usableFromInline let base: Base @usableFromInline init(_ base: Base) { self.base = base } @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base.makeIterator()) } } extension AsyncSyncSequence: Sendable where Base: Sendable { } @available(*, unavailable) extension AsyncSyncSequence.Iterator: Sendable { } ================================================ FILE: Tests/OpenAPIURLSessionTests/BufferedStreamTests/BufferedStreamTests.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// // swift-format-ignore-file //===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2020-2021 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// import XCTest @testable import OpenAPIURLSession @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class BufferedStreamTests: XCTestCase { // MARK: - sequenceDeinitialized func testSequenceDeinitialized_whenNoIterator() async throws { var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source.onTermination = { onTerminationContinuation.finish() } await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() withExtendedLifetime(stream) {} stream = nil let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) do { _ = try { try source.write(2) }() XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is AlreadyFinishedError) } group.cancelAll() } } func testSequenceDeinitialized_whenIterator() async throws { var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) var iterator = stream?.makeAsyncIterator() let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source.onTermination = { onTerminationContinuation.finish() } try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() try withExtendedLifetime(stream) { let writeResult = try source.write(1) writeResult.assertIsProducerMore() } stream = nil do { let writeResult = try { try source.write(2) }() writeResult.assertIsProducerMore() } catch { XCTFail("Expected no error to be thrown") } let element1 = try await iterator?.next() XCTAssertEqual(element1, 1) let element2 = try await iterator?.next() XCTAssertEqual(element2, 2) group.cancelAll() } } func testSequenceDeinitialized_whenFinished() async throws { var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source.onTermination = { onTerminationContinuation.finish() } await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() withExtendedLifetime(stream) { source.finish(throwing: nil) } stream = nil let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) do { _ = try { try source.write(1) }() XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is AlreadyFinishedError) } group.cancelAll() } } func testSequenceDeinitialized_whenStreaming_andSuspendedProducer() async throws { var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 2) ) _ = try { try source.write(1) }() do { try await withCheckedThrowingContinuation { continuation in source.write(1) { result in continuation.resume(with: result) } stream = nil _ = stream?.makeAsyncIterator() } } catch { XCTAssertTrue(error is AlreadyFinishedError) } } // MARK: - iteratorInitialized func testIteratorInitialized_whenInitial() async throws { let (stream, _) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) _ = stream.makeAsyncIterator() } func testIteratorInitialized_whenStreaming() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) try await source.write(1) var iterator = stream.makeAsyncIterator() let element = try await iterator.next() XCTAssertEqual(element, 1) } func testIteratorInitialized_whenSourceFinished() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) try await source.write(1) source.finish(throwing: nil) var iterator = stream.makeAsyncIterator() let element1 = try await iterator.next() XCTAssertEqual(element1, 1) let element2 = try await iterator.next() XCTAssertNil(element2) } func testIteratorInitialized_whenFinished() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) source.finish(throwing: nil) var iterator = stream.makeAsyncIterator() let element = try await iterator.next() XCTAssertNil(element) } // MARK: - iteratorDeinitialized func testIteratorDeinitialized_whenInitial() async throws { var (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source.onTermination = { onTerminationContinuation.finish() } try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() iterator = nil _ = try await iterator?.next() let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testIteratorDeinitialized_whenStreaming() async throws { var (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source.onTermination = { onTerminationContinuation.finish() } try await source.write(1) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() iterator = nil _ = try await iterator?.next() let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testIteratorDeinitialized_whenSourceFinished() async throws { var (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source.onTermination = { onTerminationContinuation.finish() } try await source.write(1) source.finish(throwing: nil) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() iterator = nil _ = try await iterator?.next() let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testIteratorDeinitialized_whenFinished() async throws { var (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source.onTermination = { onTerminationContinuation.finish() } source.finish(throwing: nil) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() iterator = nil _ = try await iterator?.next() let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testIteratorDeinitialized_whenStreaming_andSuspendedProducer() async throws { var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 2) ) var iterator: BufferedStream.AsyncIterator? = stream?.makeAsyncIterator() stream = nil _ = try { try source.write(1) }() do { try await withCheckedThrowingContinuation { continuation in source.write(1) { result in continuation.resume(with: result) } iterator = nil } } catch { XCTAssertTrue(error is AlreadyFinishedError) } _ = try await iterator?.next() } // MARK: - sourceDeinitialized func testSourceDeinitialized_whenInitial() async throws { var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source?.onTermination = { onTerminationContinuation.finish() } await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() source = nil let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } withExtendedLifetime(stream) {} } func testSourceDeinitialized_whenStreaming_andEmptyBuffer() async throws { var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source?.onTermination = { onTerminationContinuation.finish() } try await source?.write(1) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() _ = try await iterator?.next() source = nil let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testSourceDeinitialized_whenStreaming_andNotEmptyBuffer() async throws { var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source?.onTermination = { onTerminationContinuation.finish() } try await source?.write(1) try await source?.write(2) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() _ = try await iterator?.next() source = nil _ = await onTerminationIterator.next() _ = try await iterator?.next() _ = try await iterator?.next() let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testSourceDeinitialized_whenSourceFinished() async throws { var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source?.onTermination = { onTerminationContinuation.finish() } try await source?.write(1) try await source?.write(2) source?.finish(throwing: nil) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() _ = try await iterator?.next() source = nil _ = await onTerminationIterator.next() _ = try await iterator?.next() _ = try await iterator?.next() let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testSourceDeinitialized_whenFinished() async throws { var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 5, high: 10) ) let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source?.onTermination = { onTerminationContinuation.finish() } source?.finish(throwing: nil) await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() try await Task.sleep(nanoseconds: 200_000_000) } } var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() _ = stream.makeAsyncIterator() source = nil _ = await onTerminationIterator.next() let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) group.cancelAll() } } func testSourceDeinitialized_whenStreaming_andSuspendedProducer() async throws { var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 0, high: 0) ) let (producerStream, producerContinuation) = AsyncThrowingStream.makeStream() var iterator = stream.makeAsyncIterator() source?.write(1) { producerContinuation.yield(with: $0) } _ = try await iterator.next() source = nil do { try await producerStream.first { _ in true } XCTFail("We expected to throw here") } catch { XCTAssertTrue(error is AlreadyFinishedError) } } // MARK: - write func testWrite_whenInitial() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 5) ) try await source.write(1) var iterator = stream.makeAsyncIterator() let element = try await iterator.next() XCTAssertEqual(element, 1) } func testWrite_whenStreaming_andNoConsumer() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 5) ) try await source.write(1) try await source.write(2) var iterator = stream.makeAsyncIterator() let element1 = try await iterator.next() XCTAssertEqual(element1, 1) let element2 = try await iterator.next() XCTAssertEqual(element2, 2) } func testWrite_whenStreaming_andSuspendedConsumer() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 5) ) try await withThrowingTaskGroup(of: Int?.self) { group in group.addTask { return try await stream.first { _ in true } } // This is always going to be a bit racy since we need the call to next() suspend try await Task.sleep(nanoseconds: 500_000_000) try await source.write(1) let element = try await group.next() XCTAssertEqual(element, 1) } } func testWrite_whenStreaming_andSuspendedConsumer_andEmptySequence() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 5) ) try await withThrowingTaskGroup(of: Int?.self) { group in group.addTask { return try await stream.first { _ in true } } // This is always going to be a bit racy since we need the call to next() suspend try await Task.sleep(nanoseconds: 500_000_000) try await source.write(contentsOf: []) try await source.write(contentsOf: [1]) let element = try await group.next() XCTAssertEqual(element, 1) } } // MARK: - enqueueProducer func testEnqueueProducer_whenStreaming_andAndCancelled() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 2) ) let (producerStream, producerSource) = AsyncThrowingStream.makeStream() try await source.write(1) let writeResult = try { try source.write(2) }() switch writeResult { case .produceMore: preconditionFailure() case .enqueueCallback(let callbackToken): source.cancelCallback(callbackToken: callbackToken) source.enqueueCallback(callbackToken: callbackToken) { result in producerSource.yield(with: result) } } do { _ = try await producerStream.first { _ in true } XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is CancellationError) } let element = try await stream.first { _ in true } XCTAssertEqual(element, 1) } func testEnqueueProducer_whenStreaming_andAndCancelled_andAsync() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 2) ) try await source.write(1) await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await source.write(2) } group.cancelAll() do { try await group.next() XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is CancellationError) } } let element = try await stream.first { _ in true } XCTAssertEqual(element, 1) } func testEnqueueProducer_whenStreaming_andInterleaving() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 1) ) var iterator = stream.makeAsyncIterator() let (producerStream, producerSource) = AsyncThrowingStream.makeStream() let writeResult = try { try source.write(1) }() switch writeResult { case .produceMore: preconditionFailure() case .enqueueCallback(let callbackToken): let element = try await iterator.next() XCTAssertEqual(element, 1) source.enqueueCallback(callbackToken: callbackToken) { result in producerSource.yield(with: result) } } do { _ = try await producerStream.first { _ in true } } catch { XCTFail("Expected no error to be thrown") } } func testEnqueueProducer_whenStreaming_andSuspending() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 1) ) var iterator = stream.makeAsyncIterator() let (producerStream, producerSource) = AsyncThrowingStream.makeStream() let writeResult = try { try source.write(1) }() switch writeResult { case .produceMore: preconditionFailure() case .enqueueCallback(let callbackToken): source.enqueueCallback(callbackToken: callbackToken) { result in producerSource.yield(with: result) } } let element = try await iterator.next() XCTAssertEqual(element, 1) do { _ = try await producerStream.first { _ in true } } catch { XCTFail("Expected no error to be thrown") } } func testEnqueueProducer_whenFinished() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 1) ) var iterator = stream.makeAsyncIterator() let (producerStream, producerSource) = AsyncThrowingStream.makeStream() let writeResult = try { try source.write(1) }() switch writeResult { case .produceMore: preconditionFailure() case .enqueueCallback(let callbackToken): source.finish(throwing: nil) source.enqueueCallback(callbackToken: callbackToken) { result in producerSource.yield(with: result) } } let element = try await iterator.next() XCTAssertEqual(element, 1) do { _ = try await producerStream.first { _ in true } XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is AlreadyFinishedError) } } // MARK: - cancelProducer func testCancelProducer_whenStreaming() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 2) ) let (producerStream, producerSource) = AsyncThrowingStream.makeStream() try await source.write(1) let writeResult = try { try source.write(2) }() switch writeResult { case .produceMore: preconditionFailure() case .enqueueCallback(let callbackToken): source.enqueueCallback(callbackToken: callbackToken) { result in producerSource.yield(with: result) } source.cancelCallback(callbackToken: callbackToken) } do { _ = try await producerStream.first { _ in true } XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is CancellationError) } let element = try await stream.first { _ in true } XCTAssertEqual(element, 1) } func testCancelProducer_whenSourceFinished() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 2) ) let (producerStream, producerSource) = AsyncThrowingStream.makeStream() try await source.write(1) let writeResult = try { try source.write(2) }() switch writeResult { case .produceMore: preconditionFailure() case .enqueueCallback(let callbackToken): source.enqueueCallback(callbackToken: callbackToken) { result in producerSource.yield(with: result) } source.finish(throwing: nil) source.cancelCallback(callbackToken: callbackToken) } do { _ = try await producerStream.first { _ in true } XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is AlreadyFinishedError) } let element = try await stream.first { _ in true } XCTAssertEqual(element, 1) } // MARK: - finish func testFinish_whenStreaming_andConsumerSuspended() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 1) ) try await withThrowingTaskGroup(of: Int?.self) { group in group.addTask { return try await stream.first { $0 == 2 } } // This is always going to be a bit racy since we need the call to next() suspend try await Task.sleep(nanoseconds: 500_000_000) source.finish(throwing: nil) let element = try await group.next() XCTAssertEqual(element, .some(nil)) } } func testFinish_whenInitial() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 1, high: 1) ) source.finish(throwing: CancellationError()) do { for try await _ in stream {} XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is CancellationError) } } // MARK: - Backpressure func testBackPressure() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 4) ) let (backPressureEventStream, backPressureEventContinuation) = AsyncStream.makeStream( of: Void.self ) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { while true { backPressureEventContinuation.yield(()) try await source.write(contentsOf: [1]) } } var backPressureEventIterator = backPressureEventStream.makeAsyncIterator() var iterator = stream.makeAsyncIterator() await backPressureEventIterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() _ = try await iterator.next() _ = try await iterator.next() _ = try await iterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() group.cancelAll() } } func testBackPressureSync() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 4) ) let (backPressureEventStream, backPressureEventContinuation) = AsyncStream.makeStream( of: Void.self ) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @Sendable func yield() { backPressureEventContinuation.yield(()) source.write(contentsOf: [1]) { result in switch result { case .success: yield() case .failure: break } } } yield() } var backPressureEventIterator = backPressureEventStream.makeAsyncIterator() var iterator = stream.makeAsyncIterator() await backPressureEventIterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() _ = try await iterator.next() _ = try await iterator.next() _ = try await iterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() await backPressureEventIterator.next() group.cancelAll() } } func testThrowsError() async throws { let (stream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 4) ) try await source.write(1) try await source.write(2) source.finish(throwing: CancellationError()) var elements = [Int]() var iterator = stream.makeAsyncIterator() do { while let element = try await iterator.next() { elements.append(element) } XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is CancellationError) XCTAssertEqual(elements, [1, 2]) } let element = try await iterator.next() XCTAssertNil(element) } func testAsyncSequenceWrite() async throws { let (stream, continuation) = AsyncStream.makeStream() let (backpressuredStream, source) = BufferedStream.makeStream( of: Int.self, backPressureStrategy: .watermark(low: 2, high: 4) ) continuation.yield(1) continuation.yield(2) continuation.finish() try await source.write(contentsOf: stream) source.finish(throwing: nil) let elements = try await backpressuredStream.collect() XCTAssertEqual(elements, [1, 2]) } func testWatermarkBackPressureStrategy() async throws { typealias Strategy = BufferedStream._WatermarkBackPressureStrategy var strategy = Strategy(low: 2, high: 3) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didYield(elements: Slice([])), true) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didYield(elements: Slice(["*", "*"])), true) XCTAssertEqual(strategy._current, 2) XCTAssertEqual(strategy.didYield(elements: Slice(["*"])), false) XCTAssertEqual(strategy._current, 3) XCTAssertEqual(strategy.didYield(elements: Slice(["*"])), false) XCTAssertEqual(strategy._current, 4) XCTAssertEqual(strategy._current, 4) XCTAssertEqual(strategy.didConsume(elements: Slice([])), false) XCTAssertEqual(strategy._current, 4) XCTAssertEqual(strategy.didConsume(elements: Slice(["*", "*"])), false) XCTAssertEqual(strategy._current, 2) XCTAssertEqual(strategy.didConsume(elements: Slice(["*"])), true) XCTAssertEqual(strategy._current, 1) XCTAssertEqual(strategy.didConsume(elements: Slice(["*"])), true) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didConsume(elements: Slice([])), true) XCTAssertEqual(strategy._current, 0) } func testWatermarkWithoutElementCountsBackPressureStrategy() async throws { typealias Strategy = BufferedStream<[String]>._WatermarkBackPressureStrategy var strategy = Strategy(low: 2, high: 3) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didYield(elements: Slice([])), true) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didYield(elements: Slice([["*", "*"]])), true) XCTAssertEqual(strategy._current, 1) XCTAssertEqual(strategy.didYield(elements: Slice([["*", "*"]])), true) XCTAssertEqual(strategy._current, 2) XCTAssertEqual(strategy._current, 2) XCTAssertEqual(strategy.didConsume(elements: Slice([])), false) XCTAssertEqual(strategy._current, 2) XCTAssertEqual(strategy.didConsume(elements: Slice([["*", "*"]])), true) XCTAssertEqual(strategy._current, 1) XCTAssertEqual(strategy.didConsume(elements: Slice([["*", "*"]])), true) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didConsume(elements: Slice([])), true) XCTAssertEqual(strategy._current, 0) } func testWatermarkWithElementCountsBackPressureStrategy() async throws { typealias Strategy = BufferedStream<[String]>._WatermarkBackPressureStrategy var strategy = Strategy(low: 2, high: 3, waterLevelForElement: { $0.count }) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didYield(elements: Slice([])), true) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didYield(elements: Slice([["*", "*"]])), true) XCTAssertEqual(strategy._current, 2) XCTAssertEqual(strategy.didYield(elements: Slice([["*", "*"]])), false) XCTAssertEqual(strategy._current, 4) XCTAssertEqual(strategy._current, 4) XCTAssertEqual(strategy.didConsume(elements: Slice([])), false) XCTAssertEqual(strategy._current, 4) XCTAssertEqual(strategy.didConsume(elements: Slice([["*", "*"]])), false) XCTAssertEqual(strategy._current, 2) XCTAssertEqual(strategy.didConsume(elements: Slice([["*", "*"]])), true) XCTAssertEqual(strategy._current, 0) XCTAssertEqual(strategy.didConsume(elements: Slice([])), true) XCTAssertEqual(strategy._current, 0) } } extension BufferedStream.Source.WriteResult { func assertIsProducerMore() { switch self { case .produceMore: return case .enqueueCallback: XCTFail("Expected produceMore") } } func assertIsEnqueueCallback() { switch self { case .produceMore: XCTFail("Expected enqueueCallback") case .enqueueCallback: return } } } ================================================ FILE: Tests/OpenAPIURLSessionTests/NIOAsyncHTTP1TestServer.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// #if !os(Windows) // NIO not yet supported on Windows import NIOCore import NIOPosix import NIOHTTP1 @testable import OpenAPIURLSession final class AsyncTestHTTP1Server { typealias ConnectionHandler = @Sendable (NIOAsyncChannel) async throws -> Void /// Use `start(host:port:connectionHandler:)` instead. private init() {} /// Start a localhost HTTP1 server with a given connection handler. /// /// - Parameters: /// - connectionTaskGroup: Task group used to run the connection handler on new connections. /// - connectionHandler: Handler to run for each new connection. /// - Returns: The port on which the server is running. /// - Throws: If there was an error starting the server. static func start( connectionTaskGroup: inout ThrowingTaskGroup, connectionHandler: @escaping ConnectionHandler ) async throws -> Int { let group: MultiThreadedEventLoopGroup = .singleton let channel = try await ServerBootstrap(group: group) .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) .bind(host: "127.0.0.1", port: 0) { channel in channel.eventLoop.makeCompletedFuture { try channel.pipeline.syncOperations.configureHTTPServerPipeline() try channel.pipeline.syncOperations.addHandler(HTTPByteBufferResponseChannelHandler()) return try NIOAsyncChannel( wrappingChannelSynchronously: channel, configuration: NIOAsyncChannel.Configuration( inboundType: HTTPServerRequestPart.self, outboundType: HTTPServerByteBufferResponsePart.self ) ) } } connectionTaskGroup.addTask { // NOTE: it would be better to use `withThrowingDiscardingTaskGroup` here, but this would require some availablity dance and this is just used in tests. try await withThrowingTaskGroup(of: Void.self) { group in try await channel.executeThenClose { inbound, outbound in for try await connectionChannel in inbound { group.addTask { do { debug("Server handling new connection") try await connectionHandler(connectionChannel) debug("Server done handling connection") } catch { debug("Server error handling connection: \(error)") } } } } } } return channel.channel.localAddress!.port! } } /// Because `HTTPServerResponsePart` is not sendable because its body type is `IOData`, which is an abstraction over a /// `ByteBuffer` or `FileRegion`. The latter is not sendable, so we need a channel handler that deals in terms of only /// `ByteBuffer`. extension AsyncTestHTTP1Server { typealias HTTPServerByteBufferResponsePart = HTTPPart final class HTTPByteBufferResponseChannelHandler: ChannelOutboundHandler, RemovableChannelHandler { typealias OutboundIn = HTTPServerByteBufferResponsePart typealias OutboundOut = HTTPServerResponsePart func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { let part = unwrapOutboundIn(data) switch part { case .head(let head): context.write(self.wrapOutboundOut(.head(head)), promise: promise) case .body(let buffer): context.write(self.wrapOutboundOut(.body(.byteBuffer(buffer))), promise: promise) case .end(let headers): context.write(self.wrapOutboundOut(.end(headers)), promise: promise) } } } } #endif ================================================ FILE: Tests/OpenAPIURLSessionTests/TaskCancellationTests.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// #if canImport(Darwin) import Foundation import HTTPTypes import NIO import NIOHTTP1 import OpenAPIRuntime import XCTest @testable import OpenAPIURLSession enum CancellationPoint: CaseIterable { case beforeSendingHead case beforeSendingRequestBody case partwayThroughSendingRequestBody case beforeConsumingResponseBody case partwayThroughConsumingResponseBody case afterConsumingResponseBody } func testTaskCancelled(_ cancellationPoint: CancellationPoint, transport: URLSessionTransport) async throws { let requestPath = "/hello/world" let requestBodyElements = ["Hello,", "world!"] let requestBodySequence = MockAsyncSequence(elementsToVend: requestBodyElements, gatingProduction: true) let requestBody = HTTPBody( requestBodySequence, length: .known(Int64(requestBodyElements.joined().lengthOfBytes(using: .utf8))), iterationBehavior: .single ) let responseBodyMessage = "Hey!" let taskShouldCancel = XCTestExpectation(description: "Concurrency task cancelled") let taskCancelled = XCTestExpectation(description: "Concurrency task cancelled") try await withThrowingTaskGroup(of: Void.self) { group in let serverPort = try await AsyncTestHTTP1Server.start(connectionTaskGroup: &group) { connectionChannel in try await connectionChannel.executeThenClose { inbound, outbound in var requestPartIterator = inbound.makeAsyncIterator() var accumulatedBody = ByteBuffer() while let requestPart = try await requestPartIterator.next() { switch requestPart { case .head(let head): XCTAssertEqual(head.uri, requestPath) XCTAssertEqual(head.method, .POST) case .body(let buffer): accumulatedBody.writeImmutableBuffer(buffer) case .end: switch cancellationPoint { case .beforeConsumingResponseBody, .partwayThroughConsumingResponseBody, .afterConsumingResponseBody: XCTAssertEqual( String(decoding: accumulatedBody.readableBytesView, as: UTF8.self), requestBodyElements.joined() ) case .beforeSendingHead, .beforeSendingRequestBody, .partwayThroughSendingRequestBody: break } try await outbound.write(.head(.init(version: .http1_1, status: .ok))) try await outbound.write(.body(ByteBuffer(string: responseBodyMessage))) try await outbound.write(.end(nil)) } } } } debug("Server running on 127.0.0.1:\(serverPort)") let task = Task { if case .beforeSendingHead = cancellationPoint { taskShouldCancel.fulfill() await fulfillment(of: [taskCancelled]) } debug("Client starting request") async let (asyncResponse, asyncResponseBody) = try await transport.send( HTTPRequest(method: .post, scheme: nil, authority: nil, path: requestPath), body: requestBody, baseURL: URL(string: "http://127.0.0.1:\(serverPort)")!, operationID: "unused" ) if case .beforeSendingRequestBody = cancellationPoint { taskShouldCancel.fulfill() await fulfillment(of: [taskCancelled]) } requestBodySequence.openGate(for: 1) if case .partwayThroughSendingRequestBody = cancellationPoint { taskShouldCancel.fulfill() await fulfillment(of: [taskCancelled]) } requestBodySequence.openGate() let (response, maybeResponseBody) = try await (asyncResponse, asyncResponseBody) debug("Client received response head: \(response)") XCTAssertEqual(response.status, .ok) let responseBody = try XCTUnwrap(maybeResponseBody) if case .beforeConsumingResponseBody = cancellationPoint { taskShouldCancel.fulfill() await fulfillment(of: [taskCancelled]) } var iterator = responseBody.makeAsyncIterator() _ = try await iterator.next() if case .partwayThroughConsumingResponseBody = cancellationPoint { taskShouldCancel.fulfill() await fulfillment(of: [taskCancelled]) } while try await iterator.next() != nil { } if case .afterConsumingResponseBody = cancellationPoint { taskShouldCancel.fulfill() await fulfillment(of: [taskCancelled]) } } await fulfillment(of: [taskShouldCancel]) task.cancel() taskCancelled.fulfill() switch transport.configuration.implementation { case .buffering: switch cancellationPoint { case .beforeSendingHead, .beforeSendingRequestBody, .partwayThroughSendingRequestBody: await XCTAssertThrowsError(try await task.value) { error in XCTAssertTrue(error is CancellationError) } case .beforeConsumingResponseBody, .partwayThroughConsumingResponseBody, .afterConsumingResponseBody: try await task.value } case .streaming: switch cancellationPoint { case .beforeSendingHead: await XCTAssertThrowsError(try await task.value) { error in XCTAssertTrue(error is CancellationError) } case .beforeSendingRequestBody, .partwayThroughSendingRequestBody: await XCTAssertThrowsError(try await task.value) { error in switch error { case is CancellationError: break case is URLError: XCTAssertEqual((error as! URLError).code, .cancelled) default: XCTFail("Unexpected error: \(error)") } } case .beforeConsumingResponseBody, .partwayThroughConsumingResponseBody, .afterConsumingResponseBody: try await task.value } } group.cancelAll() } } func fulfillment( of expectations: [XCTestExpectation], timeout seconds: TimeInterval = .infinity, enforceOrder enforceOrderOfFulfillment: Bool = false, file: StaticString = #file, line: UInt = #line ) async { guard case .completed = await XCTWaiter.fulfillment( of: expectations, timeout: seconds, enforceOrder: enforceOrderOfFulfillment ) else { XCTFail("Expectation was not fulfilled", file: file, line: line) return } } extension URLSessionTransportBufferedTests { func testCancellation_beforeSendingHead() async throws { try await testTaskCancelled(.beforeSendingHead, transport: transport) } func testCancellation_beforeSendingRequestBody() async throws { try await testTaskCancelled(.beforeSendingRequestBody, transport: transport) } func testCancellation_partwayThroughSendingRequestBody() async throws { try await testTaskCancelled(.partwayThroughSendingRequestBody, transport: transport) } func testCancellation_beforeConsumingResponseBody() async throws { try await testTaskCancelled(.beforeConsumingResponseBody, transport: transport) } func testCancellation_partwayThroughConsumingResponseBody() async throws { try await testTaskCancelled(.partwayThroughConsumingResponseBody, transport: transport) } func testCancellation_afterConsumingResponseBody() async throws { try await testTaskCancelled(.afterConsumingResponseBody, transport: transport) } } extension URLSessionTransportStreamingTests { func testCancellation_beforeSendingHead() async throws { try await testTaskCancelled(.beforeSendingHead, transport: transport) } func testCancellation_beforeSendingRequestBody() async throws { try await testTaskCancelled(.beforeSendingRequestBody, transport: transport) } func testCancellation_partwayThroughSendingRequestBody() async throws { try await testTaskCancelled(.partwayThroughSendingRequestBody, transport: transport) } func testCancellation_beforeConsumingResponseBody() async throws { try await testTaskCancelled(.beforeConsumingResponseBody, transport: transport) } func testCancellation_partwayThroughConsumingResponseBody() async throws { try await testTaskCancelled(.partwayThroughConsumingResponseBody, transport: transport) } func testCancellation_afterConsumingResponseBody() async throws { try await testTaskCancelled(.afterConsumingResponseBody, transport: transport) } } #endif // canImport(Darwin) ================================================ FILE: Tests/OpenAPIURLSessionTests/TestUtils.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import Foundation #if !canImport(Darwin) && canImport(FoundationNetworking) import FoundationNetworking #endif import OpenAPIRuntime import XCTest func XCTAssertThrowsError( _ expression: @autoclosure () async throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, _ errorHandler: (_ error: any Error) -> Void = { _ in } ) async { do { _ = try await expression() XCTFail("expression did not throw", file: file, line: line) } catch { errorHandler(error) } } func XCTSkipUnlessAsync( _ expression: @autoclosure () async throws -> Bool, _ message: @autoclosure () -> String? = nil, file: StaticString = #filePath, line: UInt = #line ) async throws { let result = try await expression() try XCTSkipUnless(result, message(), file: file, line: line) } func XCTUnwrapAsync( _ expression: @autoclosure () async throws -> T?, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line ) async throws -> T { let maybeValue = try await expression() return try XCTUnwrap(maybeValue, message(), file: file, line: line) } func XCTAssertNilAsync( _ expression: @autoclosure () async throws -> Any?, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line ) async throws { let maybeValue = try await expression() XCTAssertNil(maybeValue, message(), file: file, line: line) } extension URL { var withoutPath: URL { var components = URLComponents(url: self, resolvingAgainstBaseURL: false)! components.path = "" return components.url! } } extension Collection { func chunks(of size: Int) -> [[Element]] { precondition(size > 0) var chunkStart = startIndex var results = [[Element]]() results.reserveCapacity((count - 1) / size + 1) while chunkStart < endIndex { let chunkEnd = index(chunkStart, offsetBy: size, limitedBy: endIndex) ?? endIndex results.append(Array(self[chunkStart..: @unchecked Sendable where Value: Sendable { private let lock: NSLock = { let lock = NSLock() lock.name = "com.apple.swift-openapi-urlsession.lock.LockedValueBox" return lock }() private var value: Value init(_ value: Value) { self.value = value } func withValue(_ work: (inout Value) throws -> R) rethrows -> R { lock.lock() defer { lock.unlock() } return try work(&value) } } extension AsyncSequence { /// Collect all elements in the sequence into an array. func collect() async throws -> [Element] { try await self.reduce(into: []) { accumulated, next in accumulated.append(next) } } } ================================================ FILE: Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/HTTPBodyOutputStreamTests.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// #if canImport(Darwin) import OpenAPIRuntime import XCTest @testable import OpenAPIURLSession // swift-format-ignore: AllPublicDeclarationsHaveDocumentation class HTTPBodyOutputStreamBridgeTests: XCTestCase { static override func setUp() { OpenAPIURLSession.debugLoggingEnabled = false } func testHTTPBodyOutputStreamInputOutput() async throws { let chunkSize = 71 let streamBufferSize = 37 let numBytes: UInt8 = .max // Create a HTTP body with one byte per chunk. let requestBytes = (0...numBytes).map { UInt8($0) } let requestChunks = requestBytes.chunks(of: chunkSize) let requestByteSequence = MockAsyncSequence(elementsToVend: requestChunks, gatingProduction: false) let requestBody = HTTPBody( requestByteSequence, length: .known(Int64(requestBytes.count)), iterationBehavior: .single ) // Create a pair of bound streams with a tiny buffer to be the bottleneck for backpressure. var inputStream: InputStream? var outputStream: OutputStream? Stream.getBoundStreams(withBufferSize: streamBufferSize, inputStream: &inputStream, outputStream: &outputStream) guard let inputStream, let outputStream else { fatalError("getBoundStreams did not return non-nil streams") } // Bridge the HTTP body to the output stream. let requestStream = HTTPBodyOutputStreamBridge(outputStream, requestBody) // Set up a mock delegate to drive the stream pair. let delegate = MockInputStreamDelegate(inputStream: inputStream) // Read all the data from the input stream using max bytes > stream buffer size. var data = [UInt8]() data.reserveCapacity(requestBytes.count) while let inputStreamBytes = try await delegate.waitForBytes(maxBytes: 4096) { data.append(contentsOf: inputStreamBytes) } XCTAssertEqual(data, requestBytes) // Check all bytes have been vended. XCTAssertEqual(requestByteSequence.elementsVended.count, requestByteSequence.elementsToVend.count) // Input stream delegate will have reached end of stream and closed the input stream. XCTAssertEqual(inputStream.streamStatus, .closed) XCTAssertNil(inputStream.streamError) // Check the output stream closes gracefully in response to the input stream closing. HTTPBodyOutputStreamBridge.streamQueue.asyncAndWait( execute: DispatchWorkItem { XCTAssertEqual(requestStream.outputStream.streamStatus, .closed) XCTAssertNil(requestStream.outputStream.streamError) } ) } func testHTTPBodyOutputStreamBridgeBackpressure() async throws { let chunkSize = 71 let streamBufferSize = 37 let numBytes: UInt8 = .max // Create a HTTP body with one byte per chunk. let requestBytes = (0...numBytes).map { UInt8($0) } let requestChunks = requestBytes.chunks(of: chunkSize) let requestByteSequence = MockAsyncSequence(elementsToVend: requestChunks, gatingProduction: true) let requestBody = HTTPBody( requestByteSequence, length: .known(Int64(requestBytes.count)), iterationBehavior: .single ) // Create a pair of bound streams with a tiny buffer to be the bottleneck for backpressure. var inputStream: InputStream? var outputStream: OutputStream? Stream.getBoundStreams(withBufferSize: streamBufferSize, inputStream: &inputStream, outputStream: &outputStream) guard let inputStream, let outputStream else { fatalError("getBoundStreams did not return non-nil streams") } // Bridge the HTTP body to the output stream. let requestStream = HTTPBodyOutputStreamBridge(outputStream, requestBody) // Set up a mock delegate to drive the stream pair. let delegate = MockInputStreamDelegate(inputStream: inputStream) _ = delegate // Check both streams have been opened. XCTAssertEqual(outputStream.streamStatus, .open) XCTAssertEqual(inputStream.streamStatus, .open) // At this point, because our mock async sequence that's backing the output stream is gated: // - The mock async sequence has vended zero elements. // - The output stream bridge has read nothing from from the async sequence. // - The output stream bridge has written nothing to the output stream. // - The output stream should have space available, the entire size of the buffer. XCTAssert(requestByteSequence.elementsVended.isEmpty) XCTAssertEqual(outputStream.streamStatus, .open) // XCTAssert(requestStream.bytesToWrite.isEmpty) XCTAssert(outputStream.hasSpaceAvailable) // Now we'll tell our mock sequence to let through as many bytes as it can. requestByteSequence.openGate() // After some time, the buffer will be full. let expectation = expectation(description: "output stream has no space available") HTTPBodyOutputStreamBridge.streamQueue.asyncAfter(deadline: .now() + .milliseconds(100)) { if !requestStream.outputStream.hasSpaceAvailable { expectation.fulfill() } } await fulfillment(of: [expectation], timeout: 0.5) // The underlying sequence should only have vended enough chunks to fill the buffer. XCTAssertEqual(requestByteSequence.elementsVended.count, (streamBufferSize - 1) / chunkSize + 1) } func testHTTPBodyOutputStreamPullThroughBufferOneByteBig() async throws { let chunkSize = 1 let streamBufferSize = 1 let numBytes: UInt8 = .max // Create a HTTP body with one byte per chunk. let requestBytes = (0...numBytes).map { UInt8($0) } let requestChunks = requestBytes.chunks(of: chunkSize) let requestByteSequence = MockAsyncSequence(elementsToVend: requestChunks, gatingProduction: true) let requestBody = HTTPBody( requestByteSequence, length: .known(Int64(requestBytes.count)), iterationBehavior: .single ) // Create a pair of bound streams with a tiny buffer to be the bottleneck for backpressure. var inputStream: InputStream? var outputStream: OutputStream? Stream.getBoundStreams(withBufferSize: streamBufferSize, inputStream: &inputStream, outputStream: &outputStream) guard let inputStream, let outputStream else { fatalError("getBoundStreams did not return non-nil streams") } // Bridge the HTTP body to the output stream. let requestStream = HTTPBodyOutputStreamBridge(outputStream, requestBody) // Set up a mock delegate to drive the stream pair. let delegate = MockInputStreamDelegate(inputStream: inputStream) // Read one byte at a time from the input sequence, which will make space in the buffer. for i in 0..: AsyncSequence, Sendable where Element: Sendable { var elementsToVend: [Element] private let _elementsVended: LockedValueBox<[Element]> var elementsVended: [Element] { _elementsVended.withValue { $0 } } private let gateOpeningsStream: AsyncStream private let gateOpeningsContinuation: AsyncStream.Continuation init(elementsToVend: [Element], gatingProduction: Bool) { self.elementsToVend = elementsToVend self._elementsVended = LockedValueBox([]) (self.gateOpeningsStream, self.gateOpeningsContinuation) = AsyncStream.makeStream(of: Void.self) if !gatingProduction { openGate() } } func openGate(for count: Int) { for _ in 0.. AsyncIterator { AsyncIterator( elementsToVend: elementsToVend[...], gateOpenings: gateOpeningsStream.makeAsyncIterator(), elementsVended: _elementsVended ) } final class AsyncIterator: AsyncIteratorProtocol { var elementsToVend: ArraySlice var gateOpenings: AsyncStream.Iterator var elementsVended: LockedValueBox<[Element]> init( elementsToVend: ArraySlice, gateOpenings: AsyncStream.Iterator, elementsVended: LockedValueBox<[Element]> ) { self.elementsToVend = elementsToVend self.gateOpenings = gateOpenings self.elementsVended = elementsVended } func next() async throws -> Element? { guard await gateOpenings.next() != nil else { throw CancellationError() } guard let element = elementsToVend.popFirst() else { return nil } elementsVended.withValue { $0.append(element) } return element } } } #endif // #if canImport(Darwin) ================================================ FILE: Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/MockInputStreamDelegate.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// #if canImport(Darwin) import Foundation @testable import OpenAPIURLSession /// Reads one byte at a time from the stream, regardless of how many bytes are available. /// /// Used for testing the HTTPOutputStreamBridge backpressure behaviour, without URLSession. final class MockInputStreamDelegate: NSObject, StreamDelegate { static let streamQueue = DispatchQueue(label: "MockInputStreamDelegate", autoreleaseFrequency: .workItem) private var inputStream: InputStream enum State { case noWaiter case haveWaiter(CheckedContinuation<[UInt8]?, any Error>, maxBytes: Int) case closed((any Error)?) } private(set) var state: State init(inputStream: InputStream) { self.inputStream = inputStream self.state = .noWaiter super.init() self.inputStream.delegate = self CFReadStreamSetDispatchQueue(self.inputStream as CFReadStream, Self.streamQueue) self.inputStream.open() } deinit { debug("Input stream delegate deinit") } private func readAndResumeContinuation() { dispatchPrecondition(condition: .onQueue(Self.streamQueue)) guard case .haveWaiter(let continuation, let maxBytes) = state else { preconditionFailure("Invalid state: \(state)") } guard inputStream.hasBytesAvailable else { return } let buffer = [UInt8](unsafeUninitializedCapacity: maxBytes) { buffer, count in count = inputStream.read(buffer.baseAddress!, maxLength: maxBytes) } switch buffer.count { case -1: debug("Input stream delegate error reading from stream: \(inputStream.streamError!)") inputStream.close() continuation.resume(throwing: inputStream.streamError!) case 0: debug("Input stream delegate reached end of stream; will close stream") self.close() continuation.resume(returning: nil) case let numBytesRead where numBytesRead > 0: debug("Input stream delegate read \(numBytesRead) bytes from stream: \(buffer)") continuation.resume(returning: buffer) default: preconditionFailure() } state = .noWaiter } func waitForBytes(maxBytes: Int) async throws -> [UInt8]? { if inputStream.streamStatus == .closed { state = .closed(inputStream.streamError) guard let error = inputStream.streamError else { return nil } throw error } return try await withCheckedThrowingContinuation { continuation in Self.streamQueue.async { guard case .noWaiter = self.state else { preconditionFailure() } self.state = .haveWaiter(continuation, maxBytes: maxBytes) self.readAndResumeContinuation() } } } func close(withError error: (any Error)? = nil) { self.inputStream.close() Self.streamQueue.async { self.state = .closed(error) } debug("Input stream delegate closed stream with error: \(String(describing: error))") } func stream(_ stream: Stream, handle event: Stream.Event) { dispatchPrecondition(condition: .onQueue(Self.streamQueue)) debug("Input stream delegate received event: \(event)") switch event { case .hasBytesAvailable: switch state { case .haveWaiter: readAndResumeContinuation() case .noWaiter: break case .closed: preconditionFailure() } case .errorOccurred: self.close() default: break } } } extension MockInputStreamDelegate: @unchecked Sendable {} // State synchronized using DispatchQueue. #endif // canImport(Darwin) ================================================ FILE: Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/URLSessionBidirectionalStreamingTests.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// #if canImport(Darwin) import Foundation import HTTPTypes import NIO import NIOHTTP1 import OpenAPIRuntime import XCTest @testable import OpenAPIURLSession @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) class URLSessionBidirectionalStreamingTests: XCTestCase { // swift-format-ignore: AllPublicDeclarationsHaveDocumentation static override func setUp() { OpenAPIURLSession.debugLoggingEnabled = false } func testBidirectionalEcho_PerChunkRatchet_1BChunk_1Chunks_1BUploadBuffer_1BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 1)[...], numRequestBodyChunks: 1, uploadBufferSize: 1, responseStreamWatermarks: (low: 1, high: 1) ) } func testBidirectionalEcho_PerChunkRatchet_1BChunk_10Chunks_1BUploadBuffer_1BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 1)[...], numRequestBodyChunks: 10, uploadBufferSize: 1, responseStreamWatermarks: (low: 1, high: 1) ) } func testBidirectionalEcho_PerChunkRatchet_1BChunk_10Chunks_10BUploadBuffer_1BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 1)[...], numRequestBodyChunks: 10, uploadBufferSize: 10, responseStreamWatermarks: (low: 1, high: 1) ) } func testBidirectionalEcho_PerChunkRatchet_1BChunk_10Chunks_1BUploadBuffer_10BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 1)[...], numRequestBodyChunks: 10, uploadBufferSize: 1, responseStreamWatermarks: (low: 10, high: 10) ) } func testBidirectionalEcho_PerChunkRatchet_1BChunk_10Chunks_10BUploadBuffer_10BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 1)[...], numRequestBodyChunks: 10, uploadBufferSize: 10, responseStreamWatermarks: (low: 10, high: 10) ) } func testBidirectionalEcho_PerChunkRatchet_10BChunk_10Chunks_1BUploadBuffer_1BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 10)[...], numRequestBodyChunks: 10, uploadBufferSize: 1, responseStreamWatermarks: (low: 1, high: 1) ) } func testBidirectionalEcho_PerChunkRatchet_10BChunk_10Chunks_10BUploadBuffer_1BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 10)[...], numRequestBodyChunks: 10, uploadBufferSize: 10, responseStreamWatermarks: (low: 1, high: 1) ) } func testBidirectionalEcho_PerChunkRatchet_10BChunk_10Chunks_1BUploadBuffer_10BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 10)[...], numRequestBodyChunks: 10, uploadBufferSize: 1, responseStreamWatermarks: (low: 10, high: 10) ) } func testBidirectionalEcho_PerChunkRatchet_10BChunk_10Chunks_10BUploadBuffer_10BDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 10)[...], numRequestBodyChunks: 10, uploadBufferSize: 10, responseStreamWatermarks: (low: 10, high: 10) ) } func testBidirectionalEcho_PerChunkRatchet_4kChunk_10Chunks_16kUploadBuffer_4kDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 4 * 1024)[...], numRequestBodyChunks: 10, uploadBufferSize: 16 * 1024, responseStreamWatermarks: (low: 4096, high: 4096) ) } func testBidirectionalEcho_PerChunkRatchet_1MChunk_10Chunks_16kUploadBuffer_4kDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 1 * 1024 * 1024)[...], numRequestBodyChunks: 10, uploadBufferSize: 16 * 1024, responseStreamWatermarks: (low: 4096, high: 4096) ) } func testBidirectionalEcho_PerChunkRatchet_10MChunk_10Chunks_1MUploadBuffer_1MDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 10 * 1024 * 1024)[...], numRequestBodyChunks: 10, uploadBufferSize: 1 * 1024 * 1024, responseStreamWatermarks: (low: 1 * 1024 * 1024, high: 1 * 1024 * 1024) ) } func testBidirectionalEcho_PerChunkRatchet_100kChunk_100Chunks_1MUploadBuffer_1MDownloadWatermark() async throws { try await testBidirectionalEchoPerChunkRatchet( requestBodyChunk: Array(repeating: UInt8(ascii: "*"), count: 100 * 1024)[...], numRequestBodyChunks: 100, uploadBufferSize: 1 * 1024 * 1024, responseStreamWatermarks: (low: 1 * 1024 * 1024, high: 1 * 1024 * 1024) ) } func testBidirectionalEchoPerChunkRatchet( requestBodyChunk: HTTPBody.ByteChunk, numRequestBodyChunks: Int, uploadBufferSize: Int, responseStreamWatermarks: (low: Int, high: Int) ) async throws { try await withThrowingTaskGroup(of: Void.self) { group in // Server task. let serverPort = try await AsyncTestHTTP1Server.start(connectionTaskGroup: &group) { connectionChannel in try await connectionChannel.executeThenClose { inbound, outbound in for try await requestPart in inbound { switch requestPart { case .head(_): try await outbound.write( .head( .init( version: .http1_1, status: .ok, headers: ["Content-Type": "application/octet-stream"] ) ) ) case .body(let buffer): try await outbound.write(.body(buffer)) case .end(_): try await outbound.write(.end(nil)) } } } } // Set up the request body. let (requestBodyStream, requestBodyStreamContinuation) = AsyncStream.makeStream() let requestBody = HTTPBody(requestBodyStream, length: .unknown, iterationBehavior: .single) // Start the request. async let asyncResponse = URLSession.shared.bidirectionalStreamingRequest( for: HTTPRequest( method: .post, scheme: nil, authority: nil, path: "/some/path", headerFields: [.contentType: "application/octet-stream"] ), baseURL: URL(string: "http://127.0.0.1:\(serverPort)")!, requestBody: requestBody, requestStreamBufferSize: uploadBufferSize, responseStreamWatermarks: responseStreamWatermarks ) /// At this point in the test, the server has sent the response head, which can be verified in Wireshark. /// /// A quirk of URLSession is that it won't fire the `didReceive response` callback, even if it has received /// the response head, until it has received at least one body byte, even when the server response headers /// indicate that the content-type is `application/octet-stream` and the transfer encoding is chunked. /// /// It's also worth noting that URLSession implements content sniffing so, if the content-type is absent, /// it will not call the `didReceive response` callback until it has received many more bytes. /// /// Additionally, there's no requirement on client libraries (or any intermediaries) to deliver partial /// responses to users, so the ability to affect this particular request response pattern entirely depends /// on the implementation details of the HTTP client libary. /// /// So... we send the first request chunk here, and have the server echo it back. requestBodyStreamContinuation.yield(requestBodyChunk) // We can now get the response head and the response body stream. let (response, responseBody) = try await asyncResponse XCTAssertEqual(response.status, .ok) // Consume and verify the first response chunk. var responseBodyIterator = responseBody!.makeAsyncIterator() var pendingExpectedResponseBytes = requestBodyChunk while !pendingExpectedResponseBytes.isEmpty { let responseBodyChunk = try await responseBodyIterator.next()! XCTAssertEqual(responseBodyChunk, pendingExpectedResponseBytes.prefix(responseBodyChunk.count)) pendingExpectedResponseBytes.removeFirst(responseBodyChunk.count) } // Send the remaining request chunks, one at a time, and check the echoed response chunk. for _ in 1..= responseChunk.count { debug("Client reconstructing and verifying chunk \(numProcessedChunks+1)/\(numResponseChunks)") XCTAssertEqual( ArraySlice(unprocessedBytes.readBytes(length: responseChunk.count)!), responseChunk ) unprocessedBytes.discardReadBytes() numProcessedChunks += 1 } } XCTAssertEqual(unprocessedBytes.readableBytes, 0) XCTAssertEqual(numProcessedChunks, numResponseChunks) case .count: var numBytesReceived = 0 for try await receivedResponseChunk in responseBody! { debug("Client received some response body bytes (numBytes: \(receivedResponseChunk.count))") numBytesReceived += receivedResponseChunk.count } XCTAssertEqual(numBytesReceived, responseChunk.count * numResponseChunks) case .delay(let delay): for try await receivedResponseChunk in responseBody! { debug("Client received some response body bytes (numBytes: \(receivedResponseChunk.count))") debug("Client doing fake work for \(delay)s") try await Task.sleep(nanoseconds: UInt64(delay.nanoseconds)) } case .none: break } group.cancelAll() } } } #endif // canImport(Darwin) ================================================ FILE: Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift ================================================ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import Foundation #if !canImport(Darwin) && canImport(FoundationNetworking) import FoundationNetworking #endif import HTTPTypes #if !os(Windows) // NIO not yet supported on Windows import NIO import NIOHTTP1 #endif import OpenAPIRuntime import XCTest @testable import OpenAPIURLSession // swift-format-ignore: AllPublicDeclarationsHaveDocumentation class URLSessionTransportConverterTests: XCTestCase { static override func setUp() { OpenAPIURLSession.debugLoggingEnabled = false } func testRequestConversion() async throws { var request = HTTPRequest( method: .post, scheme: nil, authority: nil, path: "/hello%20world/Maria?greeting=Howdy", headerFields: [.init("x-mumble2")!: "mumble", .init("x-mumble2")!: "mumble"] ) let cookie = "uid=urlsession; sid=0123456789-9876543210" request.headerFields[.cookie] = cookie request.headerFields[.init("X-Emoji")!] = "😀" let urlRequest = try URLRequest(request, baseURL: URL(string: "http://example.com/api")!) XCTAssertEqual(urlRequest.url, URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")) XCTAssertEqual(urlRequest.httpMethod, "POST") XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 3) XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble, mumble") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "cookie"), cookie) XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Emoji"), "😀") } func testResponseConversion() async throws { let urlResponse: URLResponse = HTTPURLResponse( url: URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")!, statusCode: 201, httpVersion: "HTTP/1.1", headerFields: ["x-mumble3": "mumble"] )! let response = try HTTPResponse(urlResponse) XCTAssertEqual(response.status.code, 201) XCTAssertEqual(response.headerFields, [.init("x-mumble3")!: "mumble"]) } } #if !os(Windows) // NIO not yet supported on Windows // swift-format-ignore: AllPublicDeclarationsHaveDocumentation class URLSessionTransportBufferedTests: XCTestCase { var transport: URLSessionTransport! static override func setUp() { OpenAPIURLSession.debugLoggingEnabled = false } override func setUp() async throws { transport = URLSessionTransport(configuration: .init(implementation: .buffering)) } func testBasicGet() async throws { try await testHTTPBasicGet(transport: transport) } func testBasicPost() async throws { try await testHTTPBasicPost(transport: transport) } #if canImport(Darwin) // Only passes on Darwin because Linux doesn't replay the request body on 307. func testHTTPRedirect_multipleIterationBehavior_succeeds() async throws { try await testHTTPRedirect( transport: transport, requestBodyIterationBehavior: .multiple, expectFailureDueToIterationBehavior: false ) } func testHTTPRedirect_singleIterationBehavior_succeeds() async throws { try await testHTTPRedirect( transport: transport, requestBodyIterationBehavior: .single, expectFailureDueToIterationBehavior: false ) } #endif } // swift-format-ignore: AllPublicDeclarationsHaveDocumentation class URLSessionTransportStreamingTests: XCTestCase { var transport: URLSessionTransport! static override func setUp() { OpenAPIURLSession.debugLoggingEnabled = false } override func setUpWithError() throws { try XCTSkipUnless(URLSessionTransport.Configuration.Implementation.platformSupportsStreaming) self.transport = URLSessionTransport( configuration: .init( implementation: .streaming( requestBodyStreamBufferSize: 16 * 1024, responseBodyStreamWatermarks: (low: 16 * 1024, high: 32 * 1024) ) ) ) } func testBasicGet() async throws { try await testHTTPBasicGet(transport: transport) } func testBasicPost() async throws { try await testHTTPBasicPost(transport: transport) } #if canImport(Darwin) // Only passes on Darwin because Linux doesn't replay the request body on 307. func testHTTPRedirect_multipleIterationBehavior_succeeds() async throws { try await testHTTPRedirect( transport: transport, requestBodyIterationBehavior: .multiple, expectFailureDueToIterationBehavior: false ) } func testHTTPRedirect_singleIterationBehavior_fails() async throws { try await testHTTPRedirect( transport: transport, requestBodyIterationBehavior: .single, expectFailureDueToIterationBehavior: true ) } #endif } func testHTTPRedirect( transport: any ClientTransport, requestBodyIterationBehavior: IterationBehavior, expectFailureDueToIterationBehavior: Bool ) async throws { let requestBodyChunks = ["✊", "✊", " ", "knock", " ", "knock!"] let requestBody = HTTPBody( requestBodyChunks.async, length: .known(Int64(requestBodyChunks.joined().lengthOfBytes(using: .utf8))), iterationBehavior: requestBodyIterationBehavior ) try await withThrowingTaskGroup(of: Void.self) { group in let serverPort = try await AsyncTestHTTP1Server.start(connectionTaskGroup: &group) { connectionChannel in try await connectionChannel.executeThenClose { inbound, outbound in var requestPartIterator = inbound.makeAsyncIterator() var currentURI: String? = nil var accumulatedBody = ByteBuffer() while let requestPart = try await requestPartIterator.next() { switch requestPart { case .head(let head): debug("Server received head for \(head.uri)") currentURI = head.uri case .body(let buffer): let currentURI = try XCTUnwrap(currentURI) debug("Server received body bytes for \(currentURI) (numBytes: \(buffer.readableBytes))") accumulatedBody.writeImmutableBuffer(buffer) case .end: let currentURI = try XCTUnwrap(currentURI) debug("Server received end for \(currentURI)") XCTAssertEqual(accumulatedBody, ByteBuffer(string: requestBodyChunks.joined())) switch currentURI { case "/old": debug("Server reseting body buffer") accumulatedBody = ByteBuffer() try await outbound.write( .head( .init(version: .http1_1, status: .temporaryRedirect, headers: ["Location": "/new"]) ) ) debug("Server sent head for \(currentURI)") try await outbound.write(.end(nil)) debug("Server sent end for \(currentURI)") case "/new": try await outbound.write(.head(.init(version: .http1_1, status: .ok))) debug("Server sent head for \(currentURI)") try await outbound.write(.end(nil)) debug("Server sent end for \(currentURI)") default: preconditionFailure() } } } } } debug("Server running on 127.0.0.1:\(serverPort)") // Send the request. debug("Client starting request") if expectFailureDueToIterationBehavior { await XCTAssertThrowsError( try await transport.send( HTTPRequest(method: .post, scheme: nil, authority: nil, path: "/old"), body: requestBody, baseURL: URL(string: "http://127.0.0.1:\(serverPort)")!, operationID: "unused" ) ) { error in XCTAssertEqual((error as? URLError)?.code, .cancelled, "Unexpected error: \(error)") } } else { let (response, _) = try await transport.send( HTTPRequest(method: .post, scheme: nil, authority: nil, path: "/old"), body: requestBody, baseURL: URL(string: "http://127.0.0.1:\(serverPort)")!, operationID: "unused" ) debug("Client received response head: \(response)") XCTAssertEqual(response.status, .ok) } group.cancelAll() } } func testHTTPBasicGet(transport: any ClientTransport) async throws { let requestPath = "/hello/world" let responseBodyMessage = "Hey!" try await withThrowingTaskGroup(of: Void.self) { group in let serverPort = try await AsyncTestHTTP1Server.start(connectionTaskGroup: &group) { connectionChannel in try await connectionChannel.executeThenClose { inbound, outbound in var requestPartIterator = inbound.makeAsyncIterator() while let requestPart = try await requestPartIterator.next() { switch requestPart { case .head(let head): XCTAssertEqual(head.uri, requestPath) XCTAssertEqual(head.method, .GET) case .body: XCTFail("Didn't expect any request body bytes.") case .end: try await outbound.write(.head(.init(version: .http1_1, status: .ok))) try await outbound.write(.body(ByteBuffer(string: responseBodyMessage))) try await outbound.write(.end(nil)) } } } } debug("Server running on 127.0.0.1:\(serverPort)") // Send the request. debug("Client starting request") let (response, maybeResponseBody) = try await transport.send( HTTPRequest(method: .get, scheme: nil, authority: nil, path: requestPath), body: nil, baseURL: URL(string: "http://127.0.0.1:\(serverPort)")!, operationID: "unused" ) debug("Client received response head: \(response)") XCTAssertEqual(response.status, .ok) let receivedMessage = try await String(collecting: try XCTUnwrap(maybeResponseBody), upTo: .max) XCTAssertEqual(receivedMessage, responseBodyMessage) group.cancelAll() } } func testHTTPBasicPost(transport: any ClientTransport) async throws { let requestPath = "/hello/world" let requestBodyMessage = "Hello, world!" let responseBodyMessage = "Hey!" try await withThrowingTaskGroup(of: Void.self) { group in let serverPort = try await AsyncTestHTTP1Server.start(connectionTaskGroup: &group) { connectionChannel in try await connectionChannel.executeThenClose { inbound, outbound in var requestPartIterator = inbound.makeAsyncIterator() var accumulatedBody = ByteBuffer() while let requestPart = try await requestPartIterator.next() { switch requestPart { case .head(let head): XCTAssertEqual(head.uri, requestPath) XCTAssertEqual(head.method, .POST) case .body(let buffer): accumulatedBody.writeImmutableBuffer(buffer) case .end: XCTAssertEqual(accumulatedBody, ByteBuffer(string: requestBodyMessage)) try await outbound.write(.head(.init(version: .http1_1, status: .ok))) try await outbound.write(.body(ByteBuffer(string: responseBodyMessage))) try await outbound.write(.end(nil)) } } } } debug("Server running on 127.0.0.1:\(serverPort)") // Send the request. debug("Client starting request") let (response, maybeResponseBody) = try await transport.send( HTTPRequest(method: .post, scheme: nil, authority: nil, path: requestPath), body: HTTPBody(requestBodyMessage), baseURL: URL(string: "http://127.0.0.1:\(serverPort)")!, operationID: "unused" ) debug("Client received response head: \(response)") XCTAssertEqual(response.status, .ok) let receivedMessage = try await String(collecting: try XCTUnwrap(maybeResponseBody), upTo: .max) XCTAssertEqual(receivedMessage, responseBodyMessage) group.cancelAll() } } #endif class URLSessionTransportPlatformSupportTests: XCTestCase { func testDefaultsToStreamingIfSupported() { if URLSessionTransport.Configuration.Implementation.platformSupportsStreaming { guard case .streaming = URLSessionTransport.Configuration.Implementation.platformDefault else { XCTFail() return } } else { guard case .buffering = URLSessionTransport.Configuration.Implementation.platformDefault else { XCTFail() return } } } } class URLSessionTransportDebugLoggingTests: XCTestCase { func testDebugLoggingEnabled() { let expectation = expectation(description: "message autoclosure evaluated") func message() -> String { expectation.fulfill() return "message" } OpenAPIURLSession.debugLoggingEnabled = true debug(message()) wait(for: [expectation], timeout: 0) } func testDebugLoggingDisabled() { let expectation = expectation(description: "message autoclosure evaluated") expectation.isInverted = true func message() -> String { expectation.fulfill() return "message" } OpenAPIURLSession.debugLoggingEnabled = false debug(message()) wait(for: [expectation], timeout: 0) } }