├── FlyingFox ├── Tests │ ├── Stubs │ │ ├── fish.json │ │ └── subdir │ │ │ └── vinegar.json │ ├── HTTPMethodTests.swift │ ├── HTTPClientTests.swift │ ├── AsyncSocketTests.swift │ ├── HTTPBodyPatternTests.swift │ ├── HTTPHeaderTests.swift │ ├── HTTPRequest+AddressTests.swift │ ├── HTTPResponseTests.swift │ ├── URLSession+AsyncTests.swift │ ├── AsyncSequence+ExtensionsTests.swift │ ├── HTTPResponse+Mock.swift │ └── JSON │ │ └── JSONBodyPatternTests.swift ├── XCTests │ ├── Stubs │ │ ├── fish.json │ │ └── subdir │ │ │ └── vinegar.json │ ├── XCTest+Extension.swift │ ├── HTTPMethodTests.swift │ ├── SocketAddress+Glibc.swift │ ├── HTTPResponseTests.swift │ ├── HTTPClientTests.swift │ ├── HTTPHeaderTests.swift │ ├── AsyncSocketTests.swift │ ├── HTTPBodyPatternTests.swift │ ├── HTTPRequest+AddressTests.swift │ ├── HTTPRequestTests.swift │ ├── URLSession+AsyncTests.swift │ ├── HTTPRequest+Mock.swift │ ├── AsyncSequence+ExtensionsTests.swift │ └── HTTPResponse+Mock.swift └── Sources │ ├── HTTPBodyPattern.swift │ ├── HTTPLogging+OSLog.swift │ ├── HTTPVersion.swift │ ├── NonisolatedUnsafe.swift │ ├── Handlers │ ├── ClosureHTTPHandler.swift │ └── DirectoryHTTPHandler.swift │ ├── SocketAddress+Glibc.swift │ ├── WebSocket │ ├── WSMessage.swift │ ├── AsyncStream+WSFrame.swift │ └── WSCloseCode.swift │ ├── JSON │ ├── HTTPRoute+JSONValue.swift │ ├── JSONBodyPattern.swift │ └── JSONPath.swift │ ├── HTTPRequest+RouteParameter.swift │ ├── HTTPLogging.swift │ ├── HTTPRequest+Address.swift │ ├── HTTPRequest+QueryItem.swift │ ├── JSONPredicatePattern.swift │ ├── HTTPRequest+Target.swift │ ├── HTTPClient.swift │ ├── HTTPServer+Listening.swift │ ├── URLSession+Async.swift │ ├── HTTPServer+Configuration.swift │ ├── HTTPChunkedEncodedSequence.swift │ ├── HTTPHandler.swift │ ├── HTTPHeader.swift │ └── HTTPMethod.swift ├── CSystemLinux ├── include │ ├── module.modulemap │ └── CSystemLinux.h └── shims.c ├── codecov.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── container-run-tests.sh ├── FlyingSocks ├── Tests │ ├── Resources │ │ └── JackOfHeartsRecital.txt │ ├── FileManager+TemporaryFile.swift │ ├── Logging+PrintTests.swift │ ├── Logging+OSLogTests.swift │ ├── AsyncBufferedEmptySequenceTests.swift │ ├── MutexTests.swift │ ├── SocketErrorTests.swift │ └── AsyncBufferedDataSequenceTests.swift ├── XCTests │ ├── Resources │ │ └── JackOfHeartsRecital.txt │ ├── SocketAddress+Glibc.swift │ ├── Logging+PrintTests.swift │ ├── Logging+OSLogTests.swift │ ├── AsyncBufferedEmptySequenceTests.swift │ ├── SocketErrorTests.swift │ ├── MutexXCTests.swift │ ├── AsyncBufferedFileSequenceTests.swift │ └── AsyncBufferedDataSequenceTests.swift └── Sources │ ├── SwiftSupport.swift │ ├── Transferring.swift │ ├── AsyncBufferedSequence+Extensions.swift │ ├── AsyncChunkedSequence.swift │ ├── SocketError.swift │ ├── AsyncBufferedCollection.swift │ ├── Logging+OSLog.swift │ ├── AsyncBufferedPrefixSequence.swift │ ├── AsyncBufferedSequence.swift │ ├── AsyncBufferedEmptySequence.swift │ └── Logging.swift ├── .github └── actions │ └── test-summary │ └── action.yml ├── FlyingSocks.podspec.json ├── FlyingFox.podspec.json ├── LICENSE ├── Package.swift ├── .gitignore └── Package@swift-5.10.swift /FlyingFox/Tests/Stubs/fish.json: -------------------------------------------------------------------------------- 1 | {"fish": "cakes"} -------------------------------------------------------------------------------- /FlyingFox/XCTests/Stubs/fish.json: -------------------------------------------------------------------------------- 1 | {"fish": "cakes"} -------------------------------------------------------------------------------- /FlyingFox/Tests/Stubs/subdir/vinegar.json: -------------------------------------------------------------------------------- 1 | {"type": "malt"} -------------------------------------------------------------------------------- /FlyingFox/XCTests/Stubs/subdir/vinegar.json: -------------------------------------------------------------------------------- 1 | {"type": "malt"} -------------------------------------------------------------------------------- /FlyingFox/XCTests/XCTest+Extension.swift: -------------------------------------------------------------------------------- 1 | ../../FlyingSocks/XCTests/XCTest+Extension.swift -------------------------------------------------------------------------------- /CSystemLinux/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CSystemLinux { 2 | header "CSystemLinux.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - FlyingFox/Sources/HTTPDecoder+StandardizePath.swift 3 | - FlyingFox/Tests 4 | - FlyingSocks/Tests 5 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /container-run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | container run -it \ 6 | --rm \ 7 | --mount src="$(pwd)",target=/flyingfox,type=bind \ 8 | swift:6.2 \ 9 | /usr/bin/swift test --package-path /flyingfox 10 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/Resources/JackOfHeartsRecital.txt: -------------------------------------------------------------------------------- 1 | Two doors down the boys finally made it through the wall 2 | And cleaned out the bank safe, it's said they got off with quite a haul. 3 | In the darkness by the riverbed they waited on the ground 4 | For one more member who had business back in town. 5 | But they couldn't go no further without the Jack of Hearts. 6 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/Resources/JackOfHeartsRecital.txt: -------------------------------------------------------------------------------- 1 | Two doors down the boys finally made it through the wall 2 | And cleaned out the bank safe, it's said they got off with quite a haul. 3 | In the darkness by the riverbed they waited on the ground 4 | For one more member who had business back in town. 5 | But they couldn't go no further without the Jack of Hearts. 6 | -------------------------------------------------------------------------------- /CSystemLinux/shims.c: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift System open source project 3 | Copyright (c) 2020 Apple Inc. and the Swift System project authors 4 | Licensed under Apache License v2.0 with Runtime Library Exception 5 | See https://swift.org/LICENSE.txt for license information 6 | */ 7 | 8 | #ifdef __linux__ 9 | 10 | #include 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /.github/actions/test-summary/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Test Summary' 2 | description: 'Summarise test results and coverage' 3 | inputs: 4 | junit: 5 | description: 'path to junit xml file' 6 | required: true 7 | coverage: 8 | description: 'path to lcov.json file' 9 | required: false 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: 'Summarise' 15 | run: ./.github/actions/test-summary/make-summary.swift ${{ inputs.junit }} ${{ inputs.coverage }} >> $GITHUB_STEP_SUMMARY 16 | shell: bash 17 | -------------------------------------------------------------------------------- /CSystemLinux/include/CSystemLinux.h: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift System open source project 3 | 4 | Copyright (c) 2020 Apple Inc. and the Swift System project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See https://swift.org/LICENSE.txt for license information 8 | */ 9 | 10 | #pragma once 11 | 12 | #ifdef __ANDROID__ 13 | #include 14 | #include 15 | #endif 16 | 17 | #ifdef __linux__ 18 | #include 19 | #include 20 | #endif 21 | -------------------------------------------------------------------------------- /FlyingSocks.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FlyingSocks", 3 | "version": "0.26.0", 4 | "summary": "Lightweight, async sockets written in Swift using async/await", 5 | "homepage": "https://github.com/swhitty/FlyingFox", 6 | "authors": "Simon Whitty", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "source": { 12 | "git": "https://github.com/swhitty/FlyingFox.git", 13 | "tag": "0.26.0" 14 | }, 15 | "platforms": { 16 | "ios": "13.0", 17 | "osx": "10.15", 18 | "tvos": "13.0", 19 | "watchos": "7.0" 20 | }, 21 | "source_files": "FlyingSocks/Sources/**/*.swift", 22 | "pod_target_xcconfig": { 23 | "OTHER_SWIFT_FLAGS": "-package-name FlyingFox" 24 | }, 25 | "swift_version": "5.10" 26 | } 27 | -------------------------------------------------------------------------------- /FlyingFox.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FlyingFox", 3 | "version": "0.26.0", 4 | "summary": "Lightweight, HTTP server written in Swift using async/await", 5 | "homepage": "https://github.com/swhitty/FlyingFox", 6 | "authors": "Simon Whitty", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "source": { 12 | "git": "https://github.com/swhitty/FlyingFox.git", 13 | "tag": "0.26.0" 14 | }, 15 | "platforms": { 16 | "ios": "13.0", 17 | "osx": "10.15", 18 | "tvos": "13.0", 19 | "watchos": "7.0" 20 | }, 21 | "source_files": "FlyingFox/Sources/**/*.swift", 22 | "dependencies": { 23 | "FlyingSocks": "~> 0.26.0" 24 | }, 25 | "pod_target_xcconfig": { 26 | "OTHER_SWIFT_FLAGS": "-package-name FlyingFox" 27 | }, 28 | "swift_version": "5.10" 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Simon Whitty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/SwiftSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftSupport.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 31/03/2023. 6 | // Copyright © 2023 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if compiler(<6.0) 33 | #warning("FlyingFox will soon remove support for Swift 5.10") 34 | #endif 35 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPBodyPattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPBodyPattern.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 5/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public protocol HTTPBodyPattern: Sendable { 35 | func evaluate(_ body: Data) -> Bool 36 | } 37 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPLogging+OSLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPLogging+OSLog.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 19/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingSocks 33 | 34 | #if canImport(OSLog) 35 | @available(macOS 11.0, iOS 14.0, tvOS 14.0, *) 36 | public typealias OSLogHTTPLogging = FlyingSocks.OSLogLogger 37 | #endif 38 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/Transferring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transferring.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 09/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | package struct Transferring: @unchecked Sendable { 33 | package var value: Value 34 | 35 | package init(_ value: Value) { 36 | self.value = value 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /FlyingFox/Tests/HTTPMethodTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethodTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 11/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import Foundation 34 | import Testing 35 | 36 | struct HTTPMethodTests { 37 | 38 | @Test 39 | func stringValue() { 40 | #expect( 41 | Set([HTTPMethod("fish"), .DELETE, .POST]).stringValue == "POST,DELETE,FISH" 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/SocketAddress+Glibc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketAddress+Glibc.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 15/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Glibc) 33 | // swift on linux fails to import comformance for these Glibc types 🤷🏻‍♂️: 34 | import Glibc 35 | import FlyingSocks 36 | 37 | extension sockaddr_in: SocketAddress { } 38 | 39 | extension sockaddr_in6: SocketAddress { } 40 | 41 | extension sockaddr_un: SocketAddress { } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPMethodTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethodTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 11/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import Foundation 34 | import XCTest 35 | 36 | final class HTTPMethodTests: XCTestCase { 37 | 38 | func testStringValue() { 39 | XCTAssertEqual( 40 | Set([HTTPMethod("fish"), .DELETE, .POST]).stringValue, 41 | "POST,DELETE,FISH" 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPVersion.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public struct HTTPVersion: Sendable, RawRepresentable, Hashable { 35 | public var rawValue: String 36 | 37 | public init(rawValue: String) { 38 | self.rawValue = rawValue 39 | } 40 | 41 | public init(_ rawValue: String) { 42 | self.init(rawValue: rawValue) 43 | } 44 | 45 | public static let http11 = HTTPVersion("HTTP/1.1") 46 | } 47 | -------------------------------------------------------------------------------- /FlyingFox/Sources/NonisolatedUnsafe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UncheckedSendable.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 17/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | @propertyWrapper 35 | public struct NonisolatedUnsafe: @unchecked Sendable { 36 | public var wrappedValue: Value 37 | 38 | public init(wrappedValue: Value) { 39 | self.wrappedValue = wrappedValue 40 | } 41 | } 42 | 43 | extension NonisolatedUnsafe: Equatable where Value: Equatable { } 44 | 45 | extension NonisolatedUnsafe: Hashable where Value: Hashable { } 46 | -------------------------------------------------------------------------------- /FlyingFox/Sources/Handlers/ClosureHTTPHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClosureHTTPHandler.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 14/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | 33 | public struct ClosureHTTPHandler: HTTPHandler { 34 | 35 | private let closure: @Sendable (HTTPRequest) async throws -> HTTPResponse 36 | 37 | public init(_ closure: @Sendable @escaping (HTTPRequest) async throws -> HTTPResponse) { 38 | self.closure = closure 39 | } 40 | 41 | public func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse { 42 | try await closure(request) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "FlyingFox", 7 | platforms: [ 8 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v8) 9 | ], 10 | products: [ 11 | .library( 12 | name: "FlyingFox", 13 | targets: ["FlyingFox"] 14 | ), 15 | .library( 16 | name: "FlyingSocks", 17 | targets: ["FlyingSocks"] 18 | ) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "FlyingFox", 23 | dependencies: ["FlyingSocks"], 24 | path: "FlyingFox/Sources", 25 | swiftSettings: .upcomingFeatures 26 | ), 27 | .target( 28 | name: "FlyingSocks", 29 | dependencies: [.target(name: "CSystemLinux", condition: .when(platforms: [.linux, .android]))], 30 | path: "FlyingSocks/Sources", 31 | swiftSettings: .upcomingFeatures 32 | ), 33 | .target( 34 | name: "CSystemLinux", 35 | path: "CSystemLinux" 36 | ), 37 | .testTarget( 38 | name: "FlyingFoxTests", 39 | dependencies: ["FlyingFox"], 40 | path: "FlyingFox/Tests", 41 | resources: [ 42 | .copy("Stubs") 43 | ], 44 | swiftSettings: .upcomingFeatures 45 | ), 46 | .testTarget( 47 | name: "FlyingSocksTests", 48 | dependencies: ["FlyingSocks"], 49 | path: "FlyingSocks/Tests", 50 | resources: [ 51 | .copy("Resources") 52 | ], 53 | swiftSettings: .upcomingFeatures 54 | ) 55 | ] 56 | ) 57 | 58 | extension Array where Element == SwiftSetting { 59 | 60 | static var upcomingFeatures: [SwiftSetting] { 61 | [ 62 | .enableUpcomingFeature("ExistentialAny"), 63 | .swiftLanguageMode(.v6) 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/AsyncBufferedSequence+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedSequence+Extensions.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 06/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | package extension AsyncBufferedSequence where Element == UInt8 { 35 | 36 | func getAllData(suggestedBuffer count: Int = 4096) async throws -> Data { 37 | var data = Data() 38 | var iterator = makeAsyncIterator() 39 | while let buffer = try await iterator.nextBuffer(suggested: count) { 40 | data.append(contentsOf: buffer) 41 | } 42 | return data 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/FileManager+TemporaryFile.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The TensorFlow Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @testable import FlyingSocks 16 | import Foundation 17 | 18 | 19 | extension FileManager { 20 | 21 | #if canImport(WinSDK) 22 | func makeTemporaryDirectory(template: String = "FlyingSocks.XXXXXX") throws -> URL { 23 | let suffix = UUID().uuidString.replacingOccurrences(of: "-", with: "").prefix(6) 24 | let url = temporaryDirectory.appendingPathComponent("FlyingSocks.\(suffix)", isDirectory: true) 25 | try createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) 26 | return url 27 | } 28 | #else 29 | func makeTemporaryDirectory(template: String = "FlyingSocks.XXXXXX") throws -> URL { 30 | let base = temporaryDirectory.path 31 | let needsSlash = base.hasSuffix("/") ? "" : "/" 32 | var tmpl = Array((base + needsSlash + template).utf8CString) 33 | 34 | let url = tmpl.withUnsafeMutableBufferPointer { buf -> URL? in 35 | guard let p = buf.baseAddress, mkdtemp(p) != nil else { return nil } 36 | let path = String(cString: p) 37 | return URL(fileURLWithPath: path, isDirectory: true) 38 | } 39 | 40 | guard let url = url else { 41 | throw SocketError.makeFailed("makeTemporaryDirectory()") 42 | } 43 | return url 44 | } 45 | #endif 46 | } 47 | -------------------------------------------------------------------------------- /FlyingFox/Sources/SocketAddress+Glibc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketAddress+Glibc.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 15/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Glibc) 33 | // swift on linux fails to import comformance for these Glibc types 🤷🏻‍♂️: 34 | import Glibc 35 | import FlyingSocks 36 | 37 | extension sockaddr_in: SocketAddress { 38 | public static let family = sa_family_t(AF_INET) 39 | } 40 | 41 | extension sockaddr_in6: SocketAddress { 42 | public static let family = sa_family_t(AF_INET6) 43 | } 44 | 45 | extension sockaddr_un: SocketAddress { 46 | public static let family = sa_family_t(AF_UNIX) 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/SocketAddress+Glibc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketAddress+Glibc.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 15/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Glibc) 33 | // swift 5 on linux fails to import comformance for these Glibc types 🤷🏻‍♂️: 34 | import Glibc 35 | import FlyingSocks 36 | 37 | extension sockaddr_in: SocketAddress { 38 | public static let family = sa_family_t(AF_INET) 39 | } 40 | 41 | extension sockaddr_in6: SocketAddress { 42 | public static let family = sa_family_t(AF_INET6) 43 | } 44 | 45 | extension sockaddr_un: SocketAddress { 46 | public static let family = sa_family_t(AF_UNIX) 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /FlyingFox/Sources/WebSocket/WSMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WSMessage.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 19/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public enum WSMessage: @unchecked Sendable, Hashable { 35 | case text(String) 36 | case data(Data) 37 | case close(WSCloseCode = .normalClosure) 38 | } 39 | 40 | public protocol WSMessageHandler: Sendable { 41 | func makeMessages(for client: AsyncStream) async throws -> AsyncStream 42 | } 43 | 44 | public struct EchoWSMessageHandler: WSMessageHandler { 45 | 46 | public init() {} 47 | 48 | public func makeMessages(for client: AsyncStream) async throws -> AsyncStream { 49 | client 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 02/04/2023. 6 | // Copyright © 2023 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import XCTest 34 | 35 | final class HTTPRequestTests: XCTestCase { 36 | 37 | func testRequestBodyData_CanBeChanged() async { 38 | // when 39 | var request = HTTPRequest.make(body: Data([0x01, 0x02])) 40 | 41 | // then 42 | await AsyncAssertEqual( 43 | try await request.bodyData, 44 | Data([0x01, 0x02]) 45 | ) 46 | 47 | // when 48 | request.setBodyData(Data([0x05, 0x06])) 49 | 50 | // then 51 | await AsyncAssertEqual( 52 | try await request.bodyData, 53 | Data([0x05, 0x06]) 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /FlyingFox/Sources/JSON/HTTPRoute+JSONValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRoute+JSONValue.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 15/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public extension HTTPRoute { 35 | 36 | /// Create a route to a request with a JSON body matching the supplued predicate. 37 | /// - Parameters: 38 | /// - string: String representing the method, path and query parameters of the route `POST /fish` 39 | /// - headers: Headers to evaluate and match 40 | /// - predicate: Predicate to evaluate body of the request via a `JSONValue` 41 | init( 42 | _ string: String, 43 | headers: [HTTPHeader: String] = [:], 44 | jsonBody predicate: @escaping @Sendable (JSONValue) throws -> Bool 45 | ) { 46 | self.init(string, headers: headers, body: .jsonValue(where: predicate)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FlyingFox/Sources/JSON/JSONBodyPattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONValuePattern.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 15/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public extension HTTPBodyPattern where Self == JSONBodyPattern { 35 | 36 | static func jsonValue(where predicate: @escaping @Sendable (JSONValue) throws -> Bool) -> JSONBodyPattern { 37 | JSONBodyPattern(predicate) 38 | } 39 | } 40 | 41 | public struct JSONBodyPattern: HTTPBodyPattern { 42 | 43 | private let predicate: @Sendable (JSONValue) throws -> Bool 44 | 45 | public init(_ predicate: @escaping @Sendable (JSONValue) throws -> Bool) { 46 | self.predicate = predicate 47 | } 48 | 49 | public func evaluate(_ body: Data) -> Bool { 50 | do { 51 | return try predicate(JSONValue(data: body)) 52 | } catch { 53 | return false 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /FlyingFox/Tests/HTTPClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClientTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 8/06/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Darwin) 33 | @_spi(Private) import struct FlyingFox._HTTPClient 34 | @testable import FlyingFox 35 | @testable import FlyingSocks 36 | import Foundation 37 | import Testing 38 | 39 | struct HTTPClientTests { 40 | 41 | @Test 42 | func client_sends_request() async throws { 43 | // given 44 | let server = HTTPServer(address: .loopback(port: 0)) 45 | let task = Task { try await server.run() } 46 | defer { task.cancel() } 47 | let client = _HTTPClient() 48 | 49 | // when 50 | let port = try await server.waitForListeningPort() 51 | let response = try await client.sendHTTPRequest(HTTPRequest.make(), to: .loopback(port: port)) 52 | 53 | // then 54 | #expect(response.statusCode == .notFound) 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClientTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 8/06/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @_spi(Private) import struct FlyingFox._HTTPClient 33 | @testable import FlyingFox 34 | @testable import FlyingSocks 35 | import XCTest 36 | import Foundation 37 | 38 | final class HTTPClientTests: XCTestCase { 39 | 40 | #if canImport(Darwin) 41 | func testClient() async throws { 42 | // given 43 | let server = HTTPServer(address: .loopback(port: 0)) 44 | let task = Task { try await server.run() } 45 | defer { task.cancel() } 46 | let client = _HTTPClient() 47 | 48 | // when 49 | let port = try await server.waitForListeningPort() 50 | let response = try await client.sendHTTPRequest(HTTPRequest.make(), to: .loopback(port: port)) 51 | 52 | // then 53 | XCTAssertEqual(response.statusCode, .notFound) 54 | } 55 | #endif 56 | 57 | } 58 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/Logging+PrintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging+PrintTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 23/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import Foundation 34 | import Testing 35 | 36 | struct LoggingTests { 37 | 38 | @Test 39 | func printLogger_SetsCategory() { 40 | let logger = PrintLogger.print(category: "Fish") 41 | 42 | #expect( 43 | logger.category == "Fish" 44 | ) 45 | } 46 | 47 | @Test 48 | func printLogger_output() { 49 | // NOTE: For now this test is only used to verify the output by manual confirmation 50 | // until Swift.print can be unit-tested or we are able to inject a mock. 51 | let logger = PrintLogger.print(category: "Fox") 52 | 53 | logger.logDebug("alpha") 54 | logger.logInfo("bravo") 55 | logger.logWarning("charlie") 56 | logger.logError("delta") 57 | logger.logCritical("echo") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/Logging+PrintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging+PrintTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 23/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import Foundation 34 | import XCTest 35 | 36 | final class LoggingTests: XCTestCase { 37 | 38 | func testPrintLogger_SetsCategory() { 39 | let logger = PrintLogger.print(category: "Fish") 40 | 41 | XCTAssertEqual( 42 | logger.category, 43 | "Fish" 44 | ) 45 | } 46 | 47 | func testPrintLogger_output() { 48 | // NOTE: For now this test is only used to verify the output by manual confirmation 49 | // until Swift.print can be unit-tested or we are able to inject a mock. 50 | let logger = PrintLogger.print(category: "Fox") 51 | 52 | logger.logDebug("alpha") 53 | logger.logInfo("bravo") 54 | logger.logWarning("charlie") 55 | logger.logError("delta") 56 | logger.logCritical("echo") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/Logging+OSLogTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging+OSLogTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Andre Jacobs on 06/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | #if canImport(OSLog) 32 | @testable import FlyingSocks 33 | import Foundation 34 | import OSLog 35 | import Testing 36 | 37 | struct LoggingOSLogTests { 38 | 39 | @Test 40 | func info() { 41 | guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) else { return } 42 | // NOTE: For now this test is only used to verify the output by manual confirmation (e.g. Console.app or log tool) 43 | // Run log tool in the terminal first and then run this unit-test: 44 | // log stream --level debug --predicate 'category == "FlyingFox"' 45 | let logger = OSLogLogger.oslog(category: "Fox") 46 | 47 | logger.logDebug("alpha") 48 | logger.logInfo("bravo") 49 | logger.logWarning("charlie") 50 | logger.logError("delta") 51 | logger.logCritical("echo") 52 | } 53 | } 54 | 55 | #endif // canImport(OSLog) 56 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/Logging+OSLogTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging+OSLogTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Andre Jacobs on 06/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | #if canImport(OSLog) 32 | @testable import FlyingSocks 33 | import Foundation 34 | import OSLog 35 | import XCTest 36 | 37 | final class LoggingOSLogTests: XCTestCase { 38 | 39 | func testInfo() { 40 | guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) else { return } 41 | // NOTE: For now this test is only used to verify the output by manual confirmation (e.g. Console.app or log tool) 42 | // Run log tool in the terminal first and then run this unit-test: 43 | // log stream --level debug --predicate 'category == "FlyingFox"' 44 | let logger = OSLogLogger.oslog(category: "Fox") 45 | 46 | logger.logDebug("alpha") 47 | logger.logInfo("bravo") 48 | logger.logWarning("charlie") 49 | logger.logError("delta") 50 | logger.logCritical("echo") 51 | } 52 | } 53 | 54 | #endif // canImport(OSLog) 55 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPHeaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeaderTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 11/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import Foundation 34 | import XCTest 35 | 36 | final class HTTPHeaderTests: XCTestCase { 37 | 38 | func testStringValue() { 39 | // given 40 | var headers = HTTPHeaders() 41 | headers[.transferEncoding] = "Identity" 42 | 43 | XCTAssertEqual( 44 | headers[.transferEncoding], 45 | "Identity" 46 | ) 47 | 48 | XCTAssertEqual( 49 | headers.values(for: .transferEncoding), 50 | ["Identity"] 51 | ) 52 | 53 | XCTAssertEqual( 54 | headers.values(for: .contentType), 55 | [] 56 | ) 57 | 58 | headers.addValue("chunked", for: .transferEncoding) 59 | 60 | XCTAssertEqual( 61 | headers[.transferEncoding], 62 | "Identity, chunked" 63 | ) 64 | 65 | XCTAssertEqual( 66 | headers.values(for: .transferEncoding), 67 | ["Identity", "chunked"] 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /FlyingFox/Tests/AsyncSocketTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSocketTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 22/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | @testable import FlyingSocks 34 | import Foundation 35 | 36 | extension AsyncSocket { 37 | 38 | static func make() async throws -> AsyncSocket { 39 | try await make(pool: .client) 40 | } 41 | 42 | static func make(pool: some AsyncSocketPool) throws -> AsyncSocket { 43 | let socket = try Socket(domain: AF_UNIX, type: .stream) 44 | return try AsyncSocket(socket: socket, pool: pool) 45 | } 46 | 47 | static func makePair() async throws -> (AsyncSocket, AsyncSocket) { 48 | try await makePair(pool: .client) 49 | } 50 | 51 | func writeString(_ string: String) async throws { 52 | try await write(string.data(using: .utf8)!) 53 | } 54 | 55 | func readString(length: Int) async throws -> String { 56 | let bytes = try await read(bytes: length) 57 | guard let string = String(data: Data(bytes), encoding: .utf8) else { 58 | throw SocketError.makeFailed("Read") 59 | } 60 | return string 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/AsyncSocketTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSocketTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 22/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | @testable import FlyingSocks 34 | import Foundation 35 | 36 | extension AsyncSocket { 37 | 38 | static func make() async throws -> AsyncSocket { 39 | try await make(pool: .client) 40 | } 41 | 42 | static func make(pool: some AsyncSocketPool) throws -> AsyncSocket { 43 | let socket = try Socket(domain: AF_UNIX, type: Socket.stream) 44 | return try AsyncSocket(socket: socket, pool: pool) 45 | } 46 | 47 | static func makePair() async throws -> (AsyncSocket, AsyncSocket) { 48 | try await makePair(pool: .client) 49 | } 50 | 51 | func writeString(_ string: String) async throws { 52 | try await write(string.data(using: .utf8)!) 53 | } 54 | 55 | func readString(length: Int) async throws -> String { 56 | let bytes = try await read(bytes: length) 57 | guard let string = String(data: Data(bytes), encoding: .utf8) else { 58 | throw SocketError.makeFailed("Read") 59 | } 60 | return string 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPRequest+RouteParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+RouteParameter.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | 33 | import Foundation 34 | 35 | public extension HTTPRequest { 36 | 37 | struct RouteParameter: Sendable, Hashable { 38 | public var name: String 39 | public var value: String 40 | 41 | public init(name: String, value: String) { 42 | self.name = name 43 | self.value = value 44 | } 45 | } 46 | 47 | /// Values extracted from the matched route and request 48 | var routeParameters: [RouteParameter] { Self.matchedRoute?.extractParameters(from: self) ?? [] } 49 | } 50 | 51 | public extension Array where Element == HTTPRequest.RouteParameter { 52 | 53 | subscript(_ name: String) -> String? { 54 | get { 55 | first { $0.name == name }?.value 56 | } 57 | } 58 | 59 | subscript(_ name: String, of type: T.Type = T.self) -> T? { 60 | guard let text = first(where: { $0.name == name })?.value, 61 | let value = try? T(parameter: text) else { 62 | return nil 63 | } 64 | return value 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPLogging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPLogging.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 19/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingSocks 33 | 34 | @available(*, unavailable, renamed: "FlyingSocks.Logging") 35 | public typealias HTTPLogging = FlyingSocks.Logging 36 | 37 | @available(*, unavailable, renamed: "FlyingSocks.PrintLogger") 38 | public typealias PrintHTTPLogger = FlyingSocks.PrintLogger 39 | 40 | public extension Logging where Self == PrintLogger { 41 | 42 | static func print(category: String = "FlyingFox") -> Self { 43 | return PrintLogger(category: category) 44 | } 45 | } 46 | 47 | extension HTTPServer { 48 | 49 | public static func defaultLogger(category: String = "FlyingFox") -> any Logging { 50 | defaultLogger(category: category, forceFallback: false) 51 | } 52 | 53 | static func defaultLogger(category: String = "FlyingFox", forceFallback: Bool) -> any Logging { 54 | guard !forceFallback, #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) else { 55 | return .print(category: category) 56 | } 57 | #if canImport(OSLog) 58 | return .oslog(category: category) 59 | #else 60 | return .print(category: category) 61 | #endif 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/AsyncChunkedSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncChunkedSequence.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 20/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | /// AsyncSequence that is able to also receive elements in chunks, instead of just one-at-a-time. 33 | @available(*, unavailable, renamed: "AsyncBufferedSequence") 34 | public protocol AsyncChunkedSequence: AsyncSequence where AsyncIterator: AsyncChunkedIteratorProtocol { 35 | 36 | } 37 | 38 | @available(*, unavailable, renamed: "AsyncBufferedIteratorProtocol") 39 | public protocol AsyncChunkedIteratorProtocol: AsyncIteratorProtocol { 40 | 41 | /// Retrieves n elements from sequence in a single array. 42 | /// - Returns: Array with the number of elements that was requested. Or Nil. 43 | mutating func nextChunk(count: Int) async throws -> [Element]? 44 | } 45 | 46 | @available(*, unavailable, renamed: "AsyncBufferedSequence") 47 | public protocol ChunkedAsyncSequence: AsyncSequence where AsyncIterator: ChunkedAsyncIteratorProtocol { 48 | 49 | } 50 | 51 | @available(*, unavailable, renamed: "AsyncBufferedIteratorProtocol") 52 | public protocol ChunkedAsyncIteratorProtocol: AsyncIteratorProtocol { 53 | mutating func nextChunk(count: Int) async throws -> [Element]? 54 | } 55 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPRequest+Address.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+Adress.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 03/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | 33 | import Foundation 34 | import FlyingSocks 35 | 36 | public extension HTTPRequest { 37 | 38 | enum Address: Sendable, Hashable { 39 | case ip4(String, port: UInt16) 40 | case ip6(String, port: UInt16) 41 | case unix(String) 42 | } 43 | 44 | var remoteIPAddress: String? { 45 | if let forwarded = headers[.xForwardedFor]?.split(separator: ",").first { 46 | return String(forwarded) 47 | } 48 | switch remoteAddress { 49 | case let .ip4(ip, port: _), 50 | let .ip6(ip, port: _): 51 | return ip 52 | case .unix, .none: 53 | return nil 54 | } 55 | } 56 | } 57 | 58 | public extension HTTPRequest.Address { 59 | 60 | static func make(from address: Socket.Address) -> Self { 61 | switch address { 62 | case let .ip4(ip, port: port): 63 | return .ip4(ip, port: port) 64 | case let .ip6(ip, port: port): 65 | return .ip6(ip, port: port) 66 | case let .unix(path): 67 | return .unix(path) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /FlyingFox/Tests/HTTPBodyPatternTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPBodyPatternTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import Foundation 34 | import Testing 35 | 36 | struct HTTPBodyPatternTests { 37 | 38 | #if canImport(Darwin) 39 | @Test 40 | func bodyArray_MatchesRoute() { 41 | let pattern = JSONPredicatePattern.json(where: "animals[1].name == 'fish'") 42 | 43 | #expect( 44 | pattern.evaluate( 45 | #""" 46 | { 47 | "animals": [ 48 | {"name": "dog"}, 49 | {"name": "fish"} 50 | ] 51 | } 52 | """#.data(using: .utf8)! 53 | ) 54 | ) 55 | 56 | #expect( 57 | !pattern.evaluate( 58 | #""" 59 | { 60 | "animals": [ 61 | {"name": "fish"}, 62 | {"name": "dog"} 63 | ] 64 | } 65 | """#.data(using: .utf8)! 66 | ) 67 | ) 68 | 69 | #expect( 70 | !pattern.evaluate( 71 | Data([0x01, 0x02]) 72 | ) 73 | ) 74 | } 75 | #endif 76 | } 77 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPBodyPatternTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPBodyPatternTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import XCTest 34 | 35 | final class HTTPBodyPatternTests: XCTestCase { 36 | 37 | #if canImport(Darwin) 38 | func testBodyArray_MatchesRoute() { 39 | let pattern = JSONPredicatePattern.json(where: "animals[1].name == 'fish'") 40 | 41 | XCTAssertTrue( 42 | pattern.evaluate( 43 | #""" 44 | { 45 | "animals": [ 46 | {"name": "dog"}, 47 | {"name": "fish"} 48 | ] 49 | } 50 | """#.data(using: .utf8)! 51 | ) 52 | ) 53 | 54 | XCTAssertFalse( 55 | pattern.evaluate( 56 | #""" 57 | { 58 | "animals": [ 59 | {"name": "fish"}, 60 | {"name": "dog"} 61 | ] 62 | } 63 | """#.data(using: .utf8)! 64 | ) 65 | ) 66 | 67 | XCTAssertFalse( 68 | pattern.evaluate( 69 | Data([0x01, 0x02]) 70 | ) 71 | ) 72 | } 73 | #endif 74 | } 75 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPRequest+QueryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+QueryItem.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 6/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | 33 | import Foundation 34 | 35 | public extension HTTPRequest { 36 | 37 | struct QueryItem: Sendable, Equatable { 38 | public var name: String 39 | public var value: String 40 | 41 | public init(name: String, value: String) { 42 | self.name = name 43 | self.value = value 44 | } 45 | } 46 | 47 | } 48 | 49 | public extension Array where Element == HTTPRequest.QueryItem { 50 | 51 | subscript(_ name: String) -> String? { 52 | get { 53 | first { $0.name == name }?.value 54 | } 55 | set { 56 | let item = newValue.map { 57 | HTTPRequest.QueryItem(name: name, value: $0) 58 | } 59 | guard let idx = firstIndex(where: { $0.name == name }) else { 60 | if let item = item { 61 | append(item) 62 | } 63 | return 64 | } 65 | 66 | if let item = item { 67 | self[idx] = item 68 | } else { 69 | remove(at: idx) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/SocketError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketError.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 19/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | #if canImport(Android) 34 | import Android 35 | #endif 36 | 37 | public enum SocketError: LocalizedError, Equatable { 38 | case failed(type: String, errno: Int32, message: String) 39 | case blocked 40 | case disconnected 41 | case unsupportedAddress 42 | case timeout(message: String) 43 | 44 | public var errorDescription: String? { 45 | switch self { 46 | case .failed(let type, let errno, let message): 47 | return "SocketError. \(type)(\(errno)): \(message)" 48 | case .blocked: 49 | return "SocketError. Blocked" 50 | case .disconnected: 51 | return "SocketError. Disconnected" 52 | case .unsupportedAddress: 53 | return "SocketError. UnsupportedAddress" 54 | case .timeout(message: let message): 55 | return "SocketError. Timeout: \(message)" 56 | } 57 | } 58 | 59 | static func makeFailed(_ type: StaticString) -> Self { 60 | .failed(type: String(describing: type), 61 | errno: errno, 62 | message: String(cString: strerror(errno))) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /FlyingFox/Sources/JSONPredicatePattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPBodyPattern.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 5/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Darwin) 33 | 34 | import Foundation 35 | 36 | @available(*, deprecated, message: "Use JSONBodyPattern") 37 | public struct JSONPredicatePattern: HTTPBodyPattern { 38 | 39 | #if compiler(>=6) 40 | nonisolated(unsafe) private var predicate: NSPredicate 41 | #else 42 | @NonisolatedUnsafe private var predicate: NSPredicate 43 | #endif 44 | 45 | public init(_ predicate: NSPredicate) { 46 | self.predicate = predicate 47 | } 48 | 49 | public func evaluate(_ body: Data) -> Bool { 50 | do { 51 | let object = try JSONSerialization.jsonObject(with: body, options: []) 52 | return predicate.evaluate(with: object) 53 | } catch { 54 | return false 55 | } 56 | } 57 | } 58 | 59 | @available(*, deprecated, message: "Use JSONBodyPattern") 60 | public extension HTTPBodyPattern where Self == JSONPredicatePattern { 61 | 62 | @available(*, deprecated, message: "Use JSONBodyPattern") 63 | static func json(where condition: String) -> JSONPredicatePattern { 64 | JSONPredicatePattern(NSPredicate(format: condition)) 65 | } 66 | } 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /FlyingFox/Tests/HTTPHeaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeadersTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 11/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import Foundation 34 | import Testing 35 | 36 | struct HTTPHeadersTests { 37 | 38 | @Test 39 | func stringValue() { 40 | // given 41 | var headers = HTTPHeaders() 42 | headers[.transferEncoding] = "Identity" 43 | 44 | #expect( 45 | headers[.transferEncoding] == "Identity" 46 | ) 47 | 48 | #expect( 49 | headers.values(for: .transferEncoding) == ["Identity"] 50 | ) 51 | 52 | #expect( 53 | headers.values(for: .contentType) == [] 54 | ) 55 | 56 | headers.addValue("chunked", for: .transferEncoding) 57 | 58 | #expect( 59 | headers[.transferEncoding] == "Identity, chunked" 60 | ) 61 | 62 | #expect( 63 | headers.values(for: .transferEncoding) == ["Identity", "chunked"] 64 | ) 65 | } 66 | 67 | @Test 68 | func values() { 69 | var headers = HTTPHeaders() 70 | headers.addValue("Fish", for: .setCookie) 71 | headers.addValue("Chips", for: .setCookie) 72 | 73 | #expect(headers[.setCookie] == "Fish") 74 | #expect(headers.values(for: .setCookie) == ["Fish", "Chips"]) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPRequest+Target.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+Target.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 08/11/2025. 6 | // Copyright © 2025 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public extension HTTPRequest { 35 | 36 | // RFC9112: e.g. /a%2Fb?q=1 37 | struct Target: Sendable, Equatable { 38 | 39 | // raw percent encoded path e.g. /fish%20chips 40 | private var _path: String 41 | 42 | // raw percent encoded query string e.g. q=fish%26chips&qty=15 43 | private var _query: String 44 | 45 | public init(path: String, query: String) { 46 | self._path = path 47 | self._query = query 48 | } 49 | 50 | public func path(percentEncoded: Bool = true) -> String { 51 | guard percentEncoded else { 52 | return _path.removingPercentEncoding ?? _path 53 | } 54 | return _path 55 | } 56 | 57 | public func query(percentEncoded: Bool = true) -> String { 58 | guard percentEncoded else { 59 | return _query.removingPercentEncoding ?? _query 60 | } 61 | return _query 62 | } 63 | 64 | public var rawValue: String { 65 | guard !_query.isEmpty else { 66 | return _path 67 | } 68 | return "\(_path)?\(_query)" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClient.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 8/06/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingSocks 33 | 34 | @_spi(Private) 35 | public struct _HTTPClient { 36 | 37 | public init() { } 38 | 39 | public func sendHTTPRequest(_ request: HTTPRequest, to address: some SocketAddress) async throws -> HTTPResponse { 40 | let socket = try await AsyncSocket.connected(to: address) 41 | try await socket.writeRequest(request) 42 | let response = try await socket.readResponse() 43 | // if streaming very large responses then you shouldn't close here 44 | // maybe better to close in deinit instead 45 | try? socket.close() 46 | return response 47 | } 48 | } 49 | 50 | package extension AsyncSocket { 51 | func writeRequest(_ request: HTTPRequest) async throws { 52 | try await write(HTTPEncoder.encodeRequest(request)) 53 | } 54 | 55 | func readResponse() async throws -> HTTPResponse { 56 | try await HTTPDecoder(sharedRequestBufferSize: 4096, sharedRequestReplaySize: 102_400).decodeResponse(from: bytes) 57 | } 58 | 59 | func writeFrame(_ frame: WSFrame) async throws { 60 | try await write(WSFrameEncoder.encodeFrame(frame)) 61 | } 62 | 63 | func readFrame() async throws -> WSFrame { 64 | try await WSFrameEncoder.decodeFrame(from: bytes) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Package@swift-5.10.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "FlyingFox", 7 | platforms: [ 8 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v8) 9 | ], 10 | products: [ 11 | .library( 12 | name: "FlyingFox", 13 | targets: ["FlyingFox"] 14 | ), 15 | .library( 16 | name: "FlyingSocks", 17 | targets: ["FlyingSocks"] 18 | ) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "FlyingFox", 23 | dependencies: ["FlyingSocks"], 24 | path: "FlyingFox/Sources", 25 | exclude: .excludeFiles, 26 | swiftSettings: .upcomingFeatures 27 | ), 28 | .testTarget( 29 | name: "FlyingFoxXCTests", 30 | dependencies: ["FlyingFox"], 31 | path: "FlyingFox/XCTests", 32 | resources: [ 33 | .copy("Stubs") 34 | ], 35 | swiftSettings: .upcomingFeatures 36 | ), 37 | .target( 38 | name: "FlyingSocks", 39 | dependencies: [.target(name: "CSystemLinux", condition: .when(platforms: [.linux]))], 40 | path: "FlyingSocks/Sources", 41 | swiftSettings: .upcomingFeatures 42 | ), 43 | .testTarget( 44 | name: "FlyingSocksXCTests", 45 | dependencies: ["FlyingSocks"], 46 | path: "FlyingSocks/XCTests", 47 | resources: [ 48 | .copy("Resources") 49 | ], 50 | swiftSettings: .upcomingFeatures 51 | ), 52 | .target( 53 | name: "CSystemLinux", 54 | path: "CSystemLinux" 55 | ) 56 | ] 57 | ) 58 | 59 | extension Array where Element == String { 60 | static var excludeFiles: [String] { 61 | #if os(Linux) 62 | ["JSONPredicatePattern.swift"] 63 | #else 64 | [] 65 | #endif 66 | } 67 | } 68 | 69 | extension Array where Element == SwiftSetting { 70 | 71 | static var upcomingFeatures: [SwiftSetting] { 72 | [ 73 | .enableUpcomingFeature("BareSlashRegexLiterals"), 74 | .enableUpcomingFeature("ConciseMagicFile"), 75 | .enableUpcomingFeature("DeprecateApplicationMain"), 76 | .enableUpcomingFeature("DisableOutwardActorInference"), 77 | .enableUpcomingFeature("ExistentialAny"), 78 | .enableUpcomingFeature("ForwardTrailingClosures"), 79 | .enableUpcomingFeature("GlobalConcurrency"), 80 | .enableUpcomingFeature("ImportObjcForwardDeclarations"), 81 | .enableUpcomingFeature("IsolatedDefaultValues"), 82 | //.enableExperimentalFeature("StrictConcurrency") 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPRequest+AddressTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+AddressTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 03/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingFox 33 | import XCTest 34 | 35 | final class HTTPRequestAddressTests: XCTestCase { 36 | 37 | typealias Address = HTTPRequest.Address 38 | 39 | func testRemoteAddress_IP4() { 40 | let request = HTTPRequest.make(remoteAddress: .ip4("fish", port: 80)) 41 | XCTAssertEqual(request.remoteAddress, .ip4("fish", port: 80)) 42 | XCTAssertEqual(request.remoteIPAddress, "fish") 43 | } 44 | 45 | func testRemoteAddress_IP6() { 46 | let request = HTTPRequest.make(remoteAddress: .ip6("chips", port: 8080)) 47 | XCTAssertEqual(request.remoteAddress, .ip6("chips", port: 8080)) 48 | XCTAssertEqual(request.remoteIPAddress, "chips") 49 | } 50 | 51 | func testRemoteAddress_Unix() { 52 | let request = HTTPRequest.make(remoteAddress: .unix("shrimp")) 53 | XCTAssertEqual(request.remoteAddress, .unix("shrimp")) 54 | XCTAssertNil(request.remoteIPAddress) 55 | } 56 | 57 | func testRemoteAddress_XForwardedFor() { 58 | let request = HTTPRequest.make( 59 | headers: [.xForwardedFor: "fish, chips"], 60 | remoteAddress: .ip4("shrimp", port: 80) 61 | ) 62 | XCTAssertEqual(request.remoteAddress, .ip4("shrimp", port: 80)) 63 | XCTAssertEqual(request.remoteIPAddress, "fish") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/AsyncBufferedEmptySequenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedEmptySequenceTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 06/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import Foundation 34 | import Testing 35 | 36 | struct AsyncBufferedEmptySequenceTests { 37 | 38 | @Test 39 | func completesImmediatley() async { 40 | var iterator = AsyncBufferedEmptySequence(completeImmediately: true) 41 | .makeAsyncIterator() 42 | 43 | #expect( 44 | await iterator.nextBuffer(suggested: 1) == nil 45 | ) 46 | } 47 | 48 | @Test 49 | func cancels_AfterWaiting() async { 50 | let task = Task { 51 | await AsyncBufferedEmptySequence(completeImmediately: false) 52 | .first { _ in true } 53 | } 54 | 55 | try? await Task.sleep(seconds: 0.05) 56 | task.cancel() 57 | #expect( 58 | await task.value == nil 59 | ) 60 | } 61 | 62 | @Test 63 | func cancels_Immediatley() async { 64 | let task = Task { 65 | try? await Task.sleep(seconds: 0.05) 66 | var iterator = AsyncBufferedEmptySequence(completeImmediately: false) 67 | .makeAsyncIterator() 68 | return await iterator.nextBuffer(suggested: 1) 69 | } 70 | 71 | task.cancel() 72 | #expect( 73 | await task.value == nil 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/AsyncBufferedEmptySequenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedEmptySequenceTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 06/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import Foundation 34 | import XCTest 35 | 36 | final class AsyncBufferedEmptySequenceTests: XCTestCase { 37 | 38 | func testCompletesImmediatley() async { 39 | var iterator = AsyncBufferedEmptySequence(completeImmediately: true) 40 | .makeAsyncIterator() 41 | 42 | await AsyncAssertNil( 43 | await iterator.nextBuffer(suggested: 1) 44 | ) 45 | } 46 | 47 | func testCancels_AfterWaiting() async { 48 | let task = Task { 49 | await AsyncBufferedEmptySequence(completeImmediately: false) 50 | .first { _ in true } 51 | } 52 | 53 | try? await Task.sleep(seconds: 0.05) 54 | task.cancel() 55 | await AsyncAssertNil( 56 | await task.value 57 | ) 58 | } 59 | 60 | func testCancels_Immediatley() async { 61 | let task = Task { 62 | try? await Task.sleep(seconds: 0.05) 63 | var iterator = AsyncBufferedEmptySequence(completeImmediately: false) 64 | .makeAsyncIterator() 65 | return await iterator.nextBuffer(suggested: 1) 66 | } 67 | 68 | task.cancel() 69 | await AsyncAssertNil( 70 | await task.value 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/SocketErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketErrorTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Andre Jacobs on 07/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import XCTest 34 | 35 | final class SocketErrorTests: XCTestCase { 36 | 37 | func testSocketError_errorDescription() { 38 | 39 | let failedType = "failed" 40 | let failedErrno: Int32 = 42 41 | let failedMessage = "failure is an option" 42 | XCTAssertEqual( 43 | SocketError.failed(type: failedType, errno: failedErrno, message: failedMessage).errorDescription, 44 | "SocketError. \(failedType)(\(failedErrno)): \(failedMessage)" 45 | ) 46 | 47 | XCTAssertEqual(SocketError.blocked.errorDescription, "SocketError. Blocked") 48 | XCTAssertEqual(SocketError.disconnected.errorDescription, "SocketError. Disconnected") 49 | XCTAssertEqual(SocketError.unsupportedAddress.errorDescription, "SocketError. UnsupportedAddress") 50 | } 51 | 52 | func testSocketError_makeFailed() { 53 | errno = EIO 54 | let socketError = SocketError.makeFailed("unit-test") 55 | switch socketError { 56 | case let .failed(type: type, errno: socketErrno, message: message): 57 | XCTAssertEqual(type, "unit-test") 58 | XCTAssertEqual(socketErrno, EIO) 59 | XCTAssertEqual(message, "Input/output error") 60 | default: 61 | XCTFail() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /FlyingFox/Tests/HTTPRequest+AddressTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+AddressTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 03/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingFox 33 | import Foundation 34 | import Testing 35 | 36 | struct HTTPRequestAddressTests { 37 | 38 | typealias Address = HTTPRequest.Address 39 | 40 | @Test 41 | func remoteAddress_IP4() { 42 | let request = HTTPRequest.make(remoteAddress: .ip4("fish", port: 80)) 43 | #expect(request.remoteAddress == .ip4("fish", port: 80)) 44 | #expect(request.remoteIPAddress == "fish") 45 | } 46 | 47 | @Test 48 | func remoteAddress_IP6() { 49 | let request = HTTPRequest.make(remoteAddress: .ip6("chips", port: 8080)) 50 | #expect(request.remoteAddress == .ip6("chips", port: 8080)) 51 | #expect(request.remoteIPAddress == "chips") 52 | } 53 | 54 | @Test 55 | func remoteAddress_Unix() { 56 | let request = HTTPRequest.make(remoteAddress: .unix("shrimp")) 57 | #expect(request.remoteAddress == .unix("shrimp")) 58 | #expect(request.remoteIPAddress == nil) 59 | } 60 | 61 | @Test 62 | func remoteAddress_XForwardedFor() { 63 | let request = HTTPRequest.make( 64 | headers: [.xForwardedFor: "fish, chips"], 65 | remoteAddress: .ip4("shrimp", port: 80) 66 | ) 67 | #expect(request.remoteAddress == .ip4("shrimp", port: 80)) 68 | #expect(request.remoteIPAddress == "fish") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /FlyingFox/Tests/HTTPResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 02/04/2023. 6 | // Copyright © 2023 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import FlyingSocks 34 | import Foundation 35 | import Testing 36 | 37 | struct HTTPResponseTests { 38 | 39 | @Test 40 | func completeBodyData() async throws { 41 | // given 42 | let response = HTTPResponse.make(body: Data([0x01, 0x02])) 43 | 44 | // then 45 | #expect( 46 | try await response.bodyData == Data([0x01, 0x02]) 47 | ) 48 | } 49 | 50 | @Test 51 | func sequenceBodyData() async throws { 52 | // given 53 | let buffer = ConsumingAsyncSequence( 54 | bytes: [0x5, 0x6] 55 | ) 56 | let sequence = HTTPBodySequence(from: buffer, count: 2, suggestedBufferSize: 2) 57 | let response = HTTPResponse.make(body: sequence) 58 | 59 | // then 60 | #expect( 61 | try await response.bodyData == Data([0x5, 0x6]) 62 | ) 63 | } 64 | 65 | @Test 66 | func webSocketBodyData() async throws { 67 | // given 68 | let response = HTTPResponse.make(webSocket: MessageFrameWSHandler.make()) 69 | 70 | // then 71 | #expect( 72 | try await response.bodyData == Data() 73 | ) 74 | } 75 | 76 | @Test 77 | func unknownRouteParameter() async { 78 | #expect(HTTPRequest.make().routeParameters["unknown"] == nil) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /FlyingFox/Tests/URLSession+AsyncTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession+AsyncTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 22/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import Foundation 34 | #if canImport(FoundationNetworking) 35 | import FoundationNetworking 36 | #endif 37 | import Testing 38 | 39 | struct URLSessionAsyncTests { 40 | 41 | @Test(.disabled("pie.dev is down")) 42 | func session_MakesRequest() async throws { 43 | var request = URLRequest(url: URL(string: "https://pie.dev/status/208")!) 44 | request.timeoutInterval = 2 45 | let (_, response) = try await URLSession.shared.data(for: request) 46 | 47 | #expect( 48 | (response as! HTTPURLResponse).statusCode == 208 49 | ) 50 | } 51 | 52 | @Test 53 | func session_ReturnsError() async throws { 54 | let request = URLRequest(url: URL(string: "https://flying.fox.invalid/")!) 55 | await #expect(throws: URLError.self) { 56 | try await URLSession.shared.data(for: request) 57 | } 58 | } 59 | 60 | @Test 61 | func session_CancelsRequest() async throws { 62 | let request = URLRequest(url: URL(string: "https://httpstat.us/200?sleep=10000")!) 63 | 64 | let task = Task { 65 | _ = try await URLSession.shared.data(for: request) 66 | } 67 | 68 | task.cancel() 69 | 70 | await #expect(throws: URLError.self) { 71 | try await task.value 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 02/04/2023. 6 | // Copyright © 2023 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import FlyingSocks 34 | import XCTest 35 | 36 | final class HTTPResponseTests: XCTestCase { 37 | 38 | func testCompleteBodyData() async { 39 | // given 40 | let response = HTTPResponse.make(body: Data([0x01, 0x02])) 41 | 42 | // then 43 | await AsyncAssertEqual( 44 | try await response.bodyData, 45 | Data([0x01, 0x02]) 46 | ) 47 | } 48 | 49 | func testSequenceBodyData() async { 50 | // given 51 | let buffer = ConsumingAsyncSequence( 52 | bytes: [0x5, 0x6] 53 | ) 54 | let sequence = HTTPBodySequence(from: buffer, count: 2, suggestedBufferSize: 2) 55 | let response = HTTPResponse.make(body: sequence) 56 | 57 | // then 58 | await AsyncAssertEqual( 59 | try await response.bodyData, 60 | Data([0x5, 0x6]) 61 | ) 62 | } 63 | 64 | func testWebSocketBodyData() async { 65 | // given 66 | let response = HTTPResponse.make(webSocket: MessageFrameWSHandler.make()) 67 | 68 | // then 69 | await AsyncAssertEqual( 70 | try await response.bodyData, 71 | Data() 72 | ) 73 | } 74 | 75 | func testUnknownRouteParameter() async { 76 | XCTAssertNil(HTTPRequest.make().routeParameters["unknown"]) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/MutexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutexTests.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 07/09/2024. 6 | // Copyright 2024 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Testing) 33 | @testable import FlyingSocks 34 | import Testing 35 | 36 | struct MutexTests { 37 | 38 | @Test 39 | func withLock_ReturnsValue() { 40 | let mutex = Mutex("fish") 41 | let val = mutex.withLock { 42 | $0 + " & chips" 43 | } 44 | #expect(val == "fish & chips") 45 | } 46 | 47 | @Test 48 | func withLock_ThrowsError() { 49 | let mutex = Mutex("fish") 50 | #expect(throws: CancellationError.self) { 51 | try mutex.withLock { _ -> Void in throw CancellationError() } 52 | } 53 | } 54 | 55 | @Test 56 | func lockIfAvailable_ReturnsValue() { 57 | let mutex = Mutex("fish") 58 | mutex.unsafeLock() 59 | #expect( 60 | mutex.withLockIfAvailable { _ in "chips" } == nil 61 | ) 62 | mutex.unsafeUnlock() 63 | #expect( 64 | mutex.withLockIfAvailable { _ in "chips" } == "chips" 65 | ) 66 | } 67 | 68 | @Test 69 | func withLockIfAvailable_ThrowsError() { 70 | let mutex = Mutex("fish") 71 | #expect(throws: CancellationError.self) { 72 | try mutex.withLockIfAvailable { _ -> Void in throw CancellationError() } 73 | } 74 | } 75 | } 76 | 77 | extension Mutex { 78 | func unsafeLock() { storage.lock() } 79 | func unsafeUnlock() { storage.unlock() } 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/URLSession+AsyncTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession+AsyncTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 22/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import XCTest 34 | import Foundation 35 | #if canImport(FoundationNetworking) 36 | import FoundationNetworking 37 | #endif 38 | 39 | final class URLSessionAsyncTests: XCTestCase { 40 | 41 | func disabled_testURLSession_MakesRequest() async throws { 42 | var request = URLRequest(url: URL(string: "https://pie.dev/status/208")!) 43 | request.timeoutInterval = 2 44 | let (_, response) = try await URLSession.shared.data(for: request) 45 | 46 | XCTAssertEqual( 47 | (response as! HTTPURLResponse).statusCode, 48 | 208 49 | ) 50 | } 51 | 52 | func testURLSession_ReturnsError() async throws { 53 | let request = URLRequest(url: URL(string: "https://flying.fox.invalid/")!) 54 | await AsyncAssertThrowsError(try await URLSession.shared.data(for: request), of: URLError.self) 55 | } 56 | 57 | func testURLSession_CancelsRequest() async throws { 58 | let request = URLRequest(url: URL(string: "https://httpstat.us/200?sleep=10000")!) 59 | 60 | let task = Task { 61 | _ = try await URLSession.shared.data(for: request) 62 | } 63 | 64 | task.cancel() 65 | 66 | await AsyncAssertThrowsError(try await task.value, of: URLError.self) { 67 | XCTAssertEqual($0.code, .cancelled) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/MutexXCTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutexXCTests.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 07/09/2024. 6 | // Copyright 2024 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if !canImport(Testing) 33 | @testable import FlyingSocks 34 | import XCTest 35 | 36 | final class MutexXCTests: XCTestCase { 37 | 38 | func testWithLock_ReturnsValue() { 39 | let mutex = Mutex("fish") 40 | let val = mutex.withLock { 41 | $0 + " & chips" 42 | } 43 | XCTAssertEqual(val, "fish & chips") 44 | } 45 | 46 | func testWithLock_ThrowsError() { 47 | let mutex = Mutex("fish") 48 | XCTAssertThrowsError(try mutex.withLock { _ -> Void in throw CancellationError() }) { 49 | _ = $0 is CancellationError 50 | } 51 | } 52 | 53 | func testLockIfAvailable_ReturnsValue() { 54 | let mutex = Mutex("fish") 55 | mutex.unsafeLock() 56 | XCTAssertNil( 57 | mutex.withLockIfAvailable { _ in "chips" } 58 | ) 59 | mutex.unsafeUnlock() 60 | XCTAssertEqual( 61 | mutex.withLockIfAvailable { _ in "chips" }, 62 | "chips" 63 | ) 64 | } 65 | 66 | func testWithLockIfAvailable_ThrowsError() { 67 | let mutex = Mutex("fish") 68 | XCTAssertThrowsError(try mutex.withLockIfAvailable { _ -> Void in throw CancellationError() }) { 69 | _ = $0 is CancellationError 70 | } 71 | } 72 | } 73 | 74 | extension Mutex { 75 | func unsafeLock() { storage.lock() } 76 | func unsafeUnlock() { storage.unlock() } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPServer+Listening.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPServer.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | import FlyingSocks 34 | 35 | extension HTTPServer { 36 | 37 | public var isListening: Bool { state != nil } 38 | 39 | public func waitUntilListening(timeout: TimeInterval = 5) async throws { 40 | try await withThrowingTimeout(seconds: timeout) { 41 | try await self.doWaitUntilListening() 42 | } 43 | } 44 | 45 | private func doWaitUntilListening() async throws { 46 | guard !isListening else { return } 47 | try await withIdentifiableThrowingContinuation(isolation: self) { 48 | appendContinuation($0) 49 | } onCancel: { id in 50 | Task { await self.cancelContinuation(with: id) } 51 | } 52 | } 53 | 54 | private func appendContinuation(_ continuation: Continuation) { 55 | waiting[continuation.id] = continuation 56 | } 57 | 58 | private func cancelContinuation(with id: Continuation.ID) { 59 | if let continuation = waiting.removeValue(forKey: id) { 60 | continuation.resume(throwing: CancellationError()) 61 | } 62 | } 63 | 64 | func isListeningDidUpdate(from previous: Bool) { 65 | guard isListening else { return } 66 | let waiting = self.waiting 67 | self.waiting = [:] 68 | 69 | for continuation in waiting.values { 70 | continuation.resume() 71 | } 72 | } 73 | 74 | typealias Continuation = IdentifiableContinuation 75 | } 76 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/SocketErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketErrorTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Andre Jacobs on 07/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import Foundation 34 | import Testing 35 | 36 | struct SocketErrorTests { 37 | 38 | @Test 39 | func socketError_errorDescription() { 40 | 41 | let failedType = "failed" 42 | let failedErrno: Int32 = 42 43 | let failedMessage = "failure is an option" 44 | #expect( 45 | SocketError.failed(type: failedType, errno: failedErrno, message: failedMessage).errorDescription == "SocketError. \(failedType)(\(failedErrno)): \(failedMessage)" 46 | ) 47 | 48 | #expect(SocketError.blocked.errorDescription == "SocketError. Blocked") 49 | #expect(SocketError.disconnected.errorDescription == "SocketError. Disconnected") 50 | #expect(SocketError.unsupportedAddress.errorDescription == "SocketError. UnsupportedAddress") 51 | #expect(SocketError.timeout(message: "fish").errorDescription == "SocketError. Timeout: fish") 52 | } 53 | 54 | @Test 55 | func socketError_makeFailed() { 56 | #if canImport(WinSDK) 57 | WSASetLastError(EIO) 58 | #else 59 | errno = EIO 60 | #endif 61 | 62 | let socketError = SocketError.makeFailed("unit-test") 63 | switch socketError { 64 | case let .failed(type: type, errno: socketErrno, message: message): 65 | #expect(type == "unit-test") 66 | #expect(socketErrno == EIO) 67 | #expect(message == "Input/output error") 68 | default: 69 | #expect(Bool(false)) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /FlyingFox/Sources/URLSession+Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession+Async.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | import FlyingSocks 34 | 35 | #if compiler(<6) && canImport(FoundationNetworking) 36 | import FoundationNetworking 37 | 38 | extension URLSession { 39 | 40 | // Ports macOS Foundation method to earlier Linux versions 41 | func data(for request: URLRequest) async throws -> (Data, URLResponse) { 42 | let state = Mutex((isCancelled: false, task: URLSessionDataTask?.none)) 43 | return try await withTaskCancellationHandler { 44 | try await withCheckedThrowingContinuation { continuation in 45 | let task = dataTask(with: request) { data, response, error in 46 | guard let data = data, let response = response else { 47 | continuation.resume(throwing: error!) 48 | return 49 | } 50 | continuation.resume(returning: (data, response)) 51 | } 52 | let shouldCancel = state.withLock { 53 | $0.task = task 54 | return $0.isCancelled 55 | } 56 | task.resume() 57 | if shouldCancel { 58 | task.cancel() 59 | } 60 | } 61 | } onCancel: { 62 | let taskToCancel = state.withLock { 63 | $0.isCancelled = true 64 | return $0.task 65 | } 66 | if let taskToCancel { 67 | taskToCancel.cancel() 68 | } 69 | } 70 | } 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /FlyingSocks/Tests/AsyncBufferedDataSequenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedDataSequenceTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 10/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import Foundation 34 | import Testing 35 | 36 | struct AsyncBufferedDataSequenceTests { 37 | 38 | @Test 39 | func sequenceBuffers() async { 40 | let buffer = AsyncBufferedCollection(bytes: [ 41 | 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 42 | 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF 43 | ]) 44 | 45 | #expect( 46 | await buffer.collectBuffers(ofLength: 5) == [ 47 | Data([0x0, 0x1, 0x2, 0x3, 0x4]), 48 | Data([0x5, 0x6, 0x7, 0x8, 0x9]), 49 | Data([0xA, 0xB, 0xC, 0xD, 0xE]), 50 | Data([0xF]) 51 | ] 52 | ) 53 | } 54 | 55 | @Test 56 | func sequenceCanBeIteratorMultipleTimes() async { 57 | let buffer = AsyncBufferedCollection(bytes: [ 58 | 0x0, 0x1, 0x2 59 | ]) 60 | 61 | #expect( 62 | await buffer.collectBuffers(ofLength: 5) == [Data([0x0, 0x1, 0x2])] 63 | ) 64 | 65 | #expect( 66 | await buffer.collectBuffers(ofLength: 5) == [Data([0x0, 0x1, 0x2])] 67 | ) 68 | } 69 | } 70 | 71 | private extension AsyncBufferedSequence { 72 | 73 | func collectBuffers(ofLength count: Int) async -> [AsyncIterator.Buffer] { 74 | var collected = [AsyncIterator.Buffer]() 75 | var iterator = makeAsyncIterator() 76 | 77 | while let buffer = try? await iterator.nextBuffer(suggested: count) { 78 | collected.append(buffer) 79 | } 80 | return collected 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /FlyingFox/Sources/WebSocket/AsyncStream+WSFrame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncStream+WSFrame.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 18/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingSocks 33 | 34 | extension AsyncThrowingStream { 35 | 36 | static func decodingFrames(from bytes: some AsyncBufferedSequence) -> Self { 37 | AsyncThrowingStream { 38 | do { 39 | return try await WSFrameEncoder.decodeFrame(from: bytes) 40 | } catch SocketError.disconnected, is SequenceTerminationError { 41 | return nil 42 | } catch { 43 | throw error 44 | } 45 | } 46 | } 47 | } 48 | 49 | extension AsyncStream { 50 | 51 | #if compiler(<6.2) 52 | typealias SendableMetatype = Any 53 | #endif 54 | 55 | static func protocolFrames(from frames: S) -> Self where S.Element == WSFrame { 56 | let iterator = Iterator(from: frames) 57 | return AsyncStream { 58 | do { 59 | return try await iterator.next() 60 | } catch { 61 | iterator.close() 62 | return .close(message: "Protocol Error") 63 | } 64 | } 65 | } 66 | 67 | private final class Iterator: @unchecked Sendable where S.Element == WSFrame { 68 | var iterator: S.AsyncIterator? 69 | 70 | init(from frames: S){ 71 | self.iterator = frames.makeAsyncIterator() 72 | } 73 | 74 | func next() async throws -> WSFrame? { 75 | try await iterator?.next() 76 | } 77 | 78 | func close() { 79 | iterator = nil 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /FlyingFox/Sources/Handlers/DirectoryHTTPHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryHTTPHandler.swift 3 | // FlyingFox 4 | // 5 | // Created by Huw Rowlands on 20/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public struct DirectoryHTTPHandler: HTTPHandler { 35 | 36 | private(set) var root: URL? 37 | let serverPath: String 38 | 39 | public init(root: URL, serverPath: String = "/") { 40 | self.root = root 41 | self.serverPath = serverPath 42 | } 43 | 44 | public init(bundle: Bundle, subPath: String = "", serverPath: String) { 45 | self.root = bundle.resourceURL?.appendingPathComponent(subPath) 46 | self.serverPath = serverPath 47 | } 48 | 49 | public func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse { 50 | guard 51 | let filePath = makeFileURL(for: request.path), 52 | let data = try? Data(contentsOf: filePath) else { 53 | return HTTPResponse(statusCode: .notFound) 54 | } 55 | 56 | return HTTPResponse( 57 | statusCode: .ok, 58 | headers: [.contentType: FileHTTPHandler.makeContentType(for: filePath.absoluteString)], 59 | body: data 60 | ) 61 | } 62 | 63 | func makeFileURL(for requestPath: String) -> URL? { 64 | let compsA = serverPath 65 | .split(separator: "/", omittingEmptySubsequences: true) 66 | .joined(separator: "/") 67 | 68 | let compsB = requestPath 69 | .split(separator: "/", omittingEmptySubsequences: true) 70 | .joined(separator: "/") 71 | 72 | guard compsB.hasPrefix(compsA) else { return nil } 73 | let subPath = String(compsB.dropFirst(compsA.count)) 74 | return root?.appendingPathComponent(subPath) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/AsyncBufferedFileSequenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedFileSequenceTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 06/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingSocks 33 | import Foundation 34 | import XCTest 35 | 36 | final class AsyncBufferedFileSequenceTests: XCTestCase { 37 | 38 | func testFileSize() { 39 | XCTAssertEqual( 40 | try AsyncBufferedFileSequence.fileSize(at: .jackOfHeartsRecital), 41 | 299 42 | ) 43 | XCTAssertThrowsError( 44 | try AsyncBufferedFileSequence.fileSize(at: URL(fileURLWithPath: "missing")) 45 | ) 46 | 47 | XCTAssertThrowsError( 48 | try AsyncBufferedFileSequence.fileSize(from: [:]) 49 | ) 50 | } 51 | 52 | func testFileHandleRead() throws { 53 | let handle = try FileHandle(forReadingFrom: .jackOfHeartsRecital) 54 | XCTAssertEqual( 55 | try handle.read(suggestedCount: 14, forceLegacy: false), 56 | "Two doors down".data(using: .utf8) 57 | ) 58 | XCTAssertEqual( 59 | try handle.read(suggestedCount: 9, forceLegacy: true), 60 | " the boys".data(using: .utf8) 61 | ) 62 | } 63 | 64 | func testReadsEntireFile() async throws { 65 | let sequence = try AsyncBufferedFileSequence(contentsOf: .jackOfHeartsRecital) 66 | 67 | await AsyncAssertEqual( 68 | try await sequence.getAllData(), 69 | try Data(contentsOf: .jackOfHeartsRecital) 70 | ) 71 | } 72 | } 73 | 74 | private extension URL { 75 | static var jackOfHeartsRecital: URL { 76 | Bundle.module.url(forResource: "Resources", withExtension: nil)! 77 | .appendingPathComponent("JackOfHeartsRecital.txt") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPServer+Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPServer+Configuration.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 06/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | import FlyingSocks 34 | 35 | public extension HTTPServer { 36 | 37 | struct Configuration: Sendable { 38 | public var address: any SocketAddress 39 | public var timeout: TimeInterval 40 | public var sharedRequestBufferSize: Int 41 | public var sharedRequestReplaySize: Int 42 | public var pool: any AsyncSocketPool 43 | public var logger: any Logging 44 | 45 | public init(address: some SocketAddress, 46 | timeout: TimeInterval = 15, 47 | sharedRequestBufferSize: Int = 4_096, 48 | sharedRequestReplaySize: Int = 2_097_152, 49 | pool: any AsyncSocketPool = HTTPServer.defaultPool(), 50 | logger: any Logging = HTTPServer.defaultLogger()) { 51 | self.address = address 52 | self.timeout = timeout 53 | self.sharedRequestBufferSize = sharedRequestBufferSize 54 | self.sharedRequestReplaySize = sharedRequestReplaySize 55 | self.pool = pool 56 | self.logger = logger 57 | } 58 | } 59 | } 60 | 61 | extension HTTPServer.Configuration { 62 | 63 | init(port: UInt16, 64 | timeout: TimeInterval = 15, 65 | logger: any Logging = HTTPServer.defaultLogger() 66 | ) { 67 | #if canImport(WinSDK) 68 | let address = sockaddr_in.inet(port: port) 69 | #else 70 | let address = sockaddr_in6.inet6(port: port) 71 | #endif 72 | self.init( 73 | address: address, 74 | timeout: timeout, 75 | logger: logger 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /FlyingSocks/XCTests/AsyncBufferedDataSequenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedDataSequenceTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 10/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingSocks 33 | import Foundation 34 | import XCTest 35 | 36 | final class AsyncBufferedDataSequenceTests: XCTestCase { 37 | 38 | func testSequence() async { 39 | let buffer = AsyncBufferedCollection(bytes: [ 40 | 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 41 | 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF 42 | ]) 43 | 44 | await AsyncAssertEqual( 45 | await buffer.collectBuffers(ofLength: 5), 46 | [ 47 | Data([0x0, 0x1, 0x2, 0x3, 0x4]), 48 | Data([0x5, 0x6, 0x7, 0x8, 0x9]), 49 | Data([0xA, 0xB, 0xC, 0xD, 0xE]), 50 | Data([0xF]) 51 | ] 52 | ) 53 | } 54 | 55 | func testSequenceCanBeIteratorMultipleTimes() async { 56 | let buffer = AsyncBufferedCollection(bytes: [ 57 | 0x0, 0x1, 0x2 58 | ]) 59 | 60 | await AsyncAssertEqual( 61 | await buffer.collectBuffers(ofLength: 5), 62 | [Data([0x0, 0x1, 0x2])] 63 | ) 64 | 65 | await AsyncAssertEqual( 66 | await buffer.collectBuffers(ofLength: 5), 67 | [Data([0x0, 0x1, 0x2])] 68 | ) 69 | } 70 | } 71 | 72 | private extension AsyncBufferedSequence { 73 | 74 | func collectBuffers(ofLength count: Int) async -> [AsyncIterator.Buffer] { 75 | var collected = [AsyncIterator.Buffer]() 76 | var iterator = makeAsyncIterator() 77 | 78 | while let buffer = try? await iterator.nextBuffer(suggested: count) { 79 | collected.append(buffer) 80 | } 81 | return collected 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPChunkedEncodedSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPChunkedTransferEncoder.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 09/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | import FlyingSocks 34 | 35 | struct HTTPChunkedTransferEncoder: AsyncBufferedSequence, Sendable 36 | where Base: AsyncBufferedSequence, 37 | Base.Element == UInt8, 38 | Base: Sendable { 39 | typealias Element = UInt8 40 | 41 | private let bytes: Base 42 | 43 | init(bytes: Base) { 44 | self.bytes = bytes 45 | } 46 | 47 | func makeAsyncIterator() -> Iterator { 48 | Iterator(bytes: bytes.makeAsyncIterator()) 49 | } 50 | } 51 | 52 | extension HTTPChunkedTransferEncoder { 53 | 54 | struct Iterator: AsyncBufferedIteratorProtocol { 55 | 56 | private var bytes: Base.AsyncIterator 57 | private var isComplete: Bool = false 58 | 59 | init(bytes: Base.AsyncIterator) { 60 | self.bytes = bytes 61 | } 62 | 63 | mutating func next() async throws -> UInt8? { 64 | fatalError("call nextBuffer(suggested:)") 65 | } 66 | 67 | mutating func nextBuffer(suggested count: Int) async throws -> [UInt8]? { 68 | guard !isComplete else { return nil } 69 | 70 | if let buffer = try await bytes.nextBuffer(suggested: count) { 71 | var response = Array(String(format:"%02X", buffer.count).utf8) 72 | response.append(contentsOf: Array("\r\n".utf8)) 73 | response.append(contentsOf: buffer) 74 | response.append(contentsOf: Array("\r\n".utf8)) 75 | return response 76 | } else { 77 | isComplete = true 78 | return Array("0\r\n\r\n".utf8) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPRequest+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+Mock.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 18/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import Foundation 34 | 35 | extension HTTPRequest { 36 | static func make(method: HTTPMethod = .GET, 37 | version: HTTPVersion = .http11, 38 | path: String = "/", 39 | query: [QueryItem] = [], 40 | headers: HTTPHeaders = [:], 41 | body: Data = Data(), 42 | remoteAddress: Address? = nil) -> Self { 43 | HTTPRequest(method: method, 44 | version: version, 45 | path: path, 46 | query: query, 47 | headers: headers, 48 | body: HTTPBodySequence(data: body), 49 | remoteAddress: remoteAddress) 50 | } 51 | 52 | static func make(method: HTTPMethod = .GET, _ url: String, headers: HTTPHeaders = [:]) -> Self { 53 | let (path, query) = HTTPDecoder.make().readComponents(from: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) 54 | return HTTPRequest.make( 55 | method: method, 56 | path: path, 57 | query: query, 58 | headers: headers 59 | ) 60 | } 61 | 62 | var bodyString: String { 63 | get async throws { 64 | try await String(decoding: bodyData, as: UTF8.self) 65 | } 66 | } 67 | } 68 | 69 | extension HTTPDecoder { 70 | static func make(sharedRequestBufferSize: Int = 128, sharedRequestReplaySize: Int = 1024) -> HTTPDecoder { 71 | HTTPDecoder( 72 | sharedRequestBufferSize: sharedRequestBufferSize, 73 | sharedRequestReplaySize: sharedRequestReplaySize 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/AsyncBufferedCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedDataSequence.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 10/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | package struct AsyncBufferedCollection: AsyncBufferedSequence where C: Sendable, C.Element: Sendable { 35 | package typealias Element = C.Element 36 | 37 | private let collection: C 38 | 39 | package init(_ collection: C) { 40 | self.collection = collection 41 | } 42 | 43 | package func makeAsyncIterator() -> Iterator { 44 | Iterator(collection: collection) 45 | } 46 | 47 | package struct Iterator: AsyncBufferedIteratorProtocol { 48 | 49 | private let collection: C 50 | private var index: C.Index 51 | 52 | init(collection: C) { 53 | self.collection = collection 54 | self.index = collection.startIndex 55 | } 56 | 57 | package mutating func next() async throws -> C.Element? { 58 | guard index < collection.endIndex else { return nil } 59 | let element = collection[index] 60 | index = collection.index(after: index) 61 | return element 62 | } 63 | 64 | package mutating func nextBuffer(suggested count: Int) async -> C.SubSequence? { 65 | guard index < collection.endIndex else { return nil } 66 | let endIndex = collection.index(index, offsetBy: count, limitedBy: collection.endIndex) ?? collection.endIndex 67 | let buffer = collection[index.. { 78 | init(bytes: some Sequence) { 79 | self.init(Data(bytes)) 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/Logging+OSLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging+OSLog.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 19/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(OSLog) 33 | import OSLog 34 | 35 | @available(macOS 11.0, iOS 14.0, tvOS 14.0, *) 36 | public struct OSLogLogger: Logging, @unchecked Sendable { 37 | 38 | private var logger: Logger 39 | 40 | public init(logger: Logger) { 41 | self.logger = logger 42 | } 43 | 44 | public func logDebug(_ debug: @autoclosure () -> String) { 45 | withoutActuallyEscaping(debug) { debug in 46 | logger.debug("\(debug(), privacy: .public)") 47 | } 48 | } 49 | 50 | public func logInfo(_ info: @autoclosure () -> String) { 51 | withoutActuallyEscaping(info) { info in 52 | logger.info("\(info(), privacy: .public)") 53 | } 54 | } 55 | 56 | public func logWarning(_ warning: @autoclosure () -> String) { 57 | withoutActuallyEscaping(warning) { warning in 58 | logger.warning("\(warning(), privacy: .public)") 59 | } 60 | } 61 | 62 | public func logError(_ error: @autoclosure () -> String) { 63 | withoutActuallyEscaping(error) { error in 64 | logger.error("\(error(), privacy: .public)") 65 | } 66 | } 67 | 68 | public func logCritical(_ critical: @autoclosure () -> String) { 69 | withoutActuallyEscaping(critical) { critical in 70 | logger.error("\(critical(), privacy: .public)") 71 | } 72 | } 73 | } 74 | 75 | @available(macOS 11.0, iOS 14.0, tvOS 14.0, *) 76 | public extension Logging where Self == OSLogLogger { 77 | 78 | static func oslog(bundle: Bundle = .main, category: String) -> Self { 79 | let logger = Logger(subsystem: bundle.bundleIdentifier ?? category, category: category) 80 | return OSLogLogger(logger: logger) 81 | } 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedPrefixSequence.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 04/02/2025. 6 | // Copyright © 2025 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | package struct AsyncBufferedPrefixSequence: AsyncBufferedSequence { 33 | package typealias Element = Base.Element 34 | 35 | private let base: Base 36 | private let count: Int 37 | 38 | package init(base: Base, count: Int) { 39 | self.base = base 40 | self.count = count 41 | } 42 | 43 | package func makeAsyncIterator() -> Iterator { 44 | Iterator(iterator: base.makeAsyncIterator(), remaining: count) 45 | } 46 | 47 | package struct Iterator: AsyncBufferedIteratorProtocol { 48 | private var iterator: Base.AsyncIterator 49 | private var remaining: Int 50 | 51 | init (iterator: Base.AsyncIterator, remaining: Int) { 52 | self.iterator = iterator 53 | self.remaining = remaining 54 | } 55 | 56 | package mutating func next() async throws -> Base.Element? { 57 | guard remaining > 0 else { return nil } 58 | 59 | if let element = try await iterator.next() { 60 | remaining -= 1 61 | return element 62 | } else { 63 | remaining = 0 64 | return nil 65 | } 66 | } 67 | 68 | package mutating func nextBuffer(suggested count: Int) async throws -> Base.AsyncIterator.Buffer? { 69 | guard remaining > 0 else { return nil } 70 | 71 | let count = Swift.min(remaining, count) 72 | if let buffer = try await iterator.nextBuffer(suggested: count) { 73 | remaining -= buffer.count 74 | return buffer 75 | } else { 76 | remaining = 0 77 | return nil 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /FlyingFox/Tests/AsyncSequence+ExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSequence+ExtensionsTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import FlyingSocks 34 | import Testing 35 | 36 | struct AsyncSequenceExtensionTests { 37 | 38 | @Test 39 | func collectStrings() async throws { 40 | var iterator = ConsumingAsyncSequence("fish,chips".data(using: .utf8)!) 41 | .collectStrings(separatedBy: ",") 42 | .makeAsyncIterator() 43 | 44 | #expect(try await iterator.next() == "fish") 45 | #expect(try await iterator.next() == "chips") 46 | #expect(try await iterator.next() == nil) 47 | } 48 | 49 | @Test 50 | func collectStringsWithTrailingSeperator() async throws { 51 | var iterator = ConsumingAsyncSequence("fish,chips,".data(using: .utf8)!) 52 | .collectStrings(separatedBy: ",") 53 | .makeAsyncIterator() 54 | 55 | #expect(try await iterator.next() == "fish") 56 | #expect(try await iterator.next() == "chips") 57 | #expect(try await iterator.next() == nil) 58 | } 59 | 60 | @Test 61 | func collectStringsWithTrailingSeperatorA() async throws { 62 | var iterator = ConsumingAsyncSequence([0x61, 0x2c, 0x62, 0x2c, 0xff]) 63 | .collectStrings(separatedBy: ",") 64 | .makeAsyncIterator() 65 | 66 | #expect(try await iterator.next() == "a") 67 | #expect(try await iterator.next() == "b") 68 | await #expect(throws: AsyncSequenceError.self) { 69 | try await iterator.next() 70 | } 71 | } 72 | 73 | @Test 74 | func takeNextThrowsError_WhenSequenceEnds() async { 75 | let sequence = ConsumingAsyncSequence(bytes: []) 76 | 77 | await #expect(throws: SequenceTerminationError.self) { 78 | try await sequence.takeNext() 79 | } 80 | } 81 | } 82 | 83 | 84 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/AsyncSequence+ExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSequence+ExtensionsTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/03/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import FlyingSocks 34 | import XCTest 35 | 36 | final class AsyncSequenceExtensionTests: XCTestCase { 37 | 38 | func testCollectStrings() async throws { 39 | var iterator = ConsumingAsyncSequence("fish,chips".data(using: .utf8)!) 40 | .collectStrings(separatedBy: ",") 41 | .makeAsyncIterator() 42 | // 43 | await AsyncAssertEqual(try await iterator.next(), "fish") 44 | await AsyncAssertEqual(try await iterator.next(), "chips") 45 | await AsyncAssertEqual(try await iterator.next(), nil) 46 | } 47 | 48 | func testCollectStringsWithTrailingSeperator() async throws { 49 | var iterator = ConsumingAsyncSequence("fish,chips,".data(using: .utf8)!) 50 | .collectStrings(separatedBy: ",") 51 | .makeAsyncIterator() 52 | 53 | await AsyncAssertEqual(try await iterator.next(), "fish") 54 | await AsyncAssertEqual(try await iterator.next(), "chips") 55 | await AsyncAssertEqual(try await iterator.next(), nil) 56 | } 57 | 58 | func testCollectStringsWithTrailingSeperatorA() async throws { 59 | var iterator = ConsumingAsyncSequence([0x61, 0x2c, 0x62, 0x2c, 0xff]) 60 | .collectStrings(separatedBy: ",") 61 | .makeAsyncIterator() 62 | 63 | await AsyncAssertEqual(try await iterator.next(), "a") 64 | await AsyncAssertEqual(try await iterator.next(), "b") 65 | await AsyncAssertThrowsError(try await iterator.next(), of: AsyncSequenceError.self) 66 | } 67 | 68 | func testTakeNextThrowsError_WhenSequenceEnds() async { 69 | let sequence = ConsumingAsyncSequence(bytes: []) 70 | 71 | await AsyncAssertThrowsError(try await sequence.takeNext(), of: SequenceTerminationError.self) 72 | } 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/AsyncBufferedSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedSequence.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 06/07/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | /// AsyncSequence that is buffered and can optionally receive contiguous elements in chunks, instead of just one-at-a-time. 33 | public protocol AsyncBufferedSequence: AsyncSequence, Sendable where AsyncIterator: AsyncBufferedIteratorProtocol, Element: Sendable { 34 | 35 | } 36 | 37 | public protocol AsyncBufferedIteratorProtocol: AsyncIteratorProtocol { 38 | // Buffered elements are returned in this collection type 39 | associatedtype Buffer: Collection where Buffer.Element == Element 40 | 41 | /// Retrieves available elements from the buffer. Suspends if 0 elements are available. 42 | /// - Parameter count: The suggested number of elements to return 43 | /// - Returns: Collection with between 1 and the number elements that was requested. Nil is returned if the sequence has ended. 44 | mutating func nextBuffer(suggested count: Int) async throws -> Buffer? 45 | } 46 | 47 | public extension AsyncBufferedIteratorProtocol { 48 | 49 | /// Retrieves n elements from sequence in a single array. 50 | /// - Parameter count: The maximum number of elements to return 51 | /// - Returns: Array with the number of elements that was requested. Nil is returned if the sequence has ended. 52 | mutating func nextBuffer(count: Int) async throws -> [Element]? { 53 | guard count > 0 else { return [] } 54 | 55 | var buffer = [Element]() 56 | while buffer.count < count { 57 | try Task.checkCancellation() 58 | let remaining = count - buffer.count 59 | if let chunk = try await nextBuffer(suggested: remaining) { 60 | buffer.append(contentsOf: chunk) 61 | } else { 62 | throw SocketError.disconnected 63 | } 64 | } 65 | return buffer.isEmpty ? nil : buffer 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/AsyncBufferedEmptySequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBufferedEmptySequence.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 06/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | package struct AsyncBufferedEmptySequence: Sendable, AsyncBufferedSequence { 33 | 34 | private let completeImmediately: Bool 35 | 36 | package init(completeImmediately: Bool = false) { 37 | self.completeImmediately = completeImmediately 38 | } 39 | 40 | package func makeAsyncIterator() -> AsyncIterator { 41 | AsyncIterator(completeImmediately: completeImmediately) 42 | } 43 | 44 | package struct AsyncIterator: AsyncBufferedIteratorProtocol { 45 | let completeImmediately: Bool 46 | 47 | package mutating func next() async -> Element? { 48 | if completeImmediately { 49 | return nil 50 | } 51 | let state = Mutex(State()) 52 | return await withTaskCancellationHandler { 53 | await withCheckedContinuation { (continuation: CheckedContinuation) in 54 | let shouldCancel = state.withLock { 55 | $0.continuation = continuation 56 | return $0.isCancelled 57 | } 58 | 59 | if shouldCancel { 60 | continuation.resume(returning: nil) 61 | } 62 | } 63 | } onCancel: { 64 | let continuation = state.withLock { 65 | $0.isCancelled = true 66 | return $0.continuation 67 | } 68 | continuation?.resume(returning: nil) 69 | } 70 | } 71 | 72 | package mutating func nextBuffer(suggested count: Int) async -> [Element]? { 73 | await next().map { [$0] } 74 | } 75 | 76 | private struct State { 77 | var continuation: CheckedContinuation? 78 | var isCancelled: Bool = false 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /FlyingFox/Tests/HTTPResponse+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPResponse+Mock.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 17/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import FlyingSocks 34 | import Foundation 35 | 36 | extension HTTPResponse { 37 | 38 | static func make(version: HTTPVersion = .http11, 39 | statusCode: HTTPStatusCode = .ok, 40 | headers: HTTPHeaders = [:], 41 | body: Data = Data()) -> Self { 42 | HTTPResponse(version: version, 43 | statusCode: statusCode, 44 | headers: headers, 45 | body: body) 46 | } 47 | 48 | static func makeChunked(version: HTTPVersion = .http11, 49 | statusCode: HTTPStatusCode = .ok, 50 | headers: HTTPHeaders = [:], 51 | body: Data = Data(), 52 | chunkSize: Int = 5) -> Self { 53 | let consuming = ConsumingAsyncSequence(body) 54 | return HTTPResponse( 55 | version: version, 56 | statusCode: statusCode, 57 | headers: headers, 58 | body: HTTPBodySequence(from: consuming, suggestedBufferSize: chunkSize) 59 | ) 60 | } 61 | 62 | static func make(version: HTTPVersion = .http11, 63 | statusCode: HTTPStatusCode = .ok, 64 | headers: HTTPHeaders = [:], 65 | body: HTTPBodySequence) -> Self { 66 | HTTPResponse(version: version, 67 | statusCode: statusCode, 68 | headers: headers, 69 | body: body) 70 | } 71 | 72 | static func make(headers: [HTTPHeader: String] = [:], 73 | webSocket handler: some WSHandler) -> Self { 74 | HTTPResponse(headers: headers, 75 | webSocket: handler) 76 | } 77 | 78 | var bodyString: String { 79 | get async throws { 80 | try await String(decoding: bodyData, as: UTF8.self) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /FlyingFox/Sources/WebSocket/WSCloseCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WSCloseCode.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 04/03/2025. 6 | // Copyright © 2025 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public struct WSCloseCode: Sendable, Hashable { 35 | public var code: UInt16 36 | public var reason: String 37 | 38 | public init(_ code: UInt16) { 39 | self.code = code 40 | self.reason = "" 41 | } 42 | public init(_ code: UInt16, reason: String) { 43 | self.code = code 44 | self.reason = reason 45 | } 46 | } 47 | 48 | public extension WSCloseCode { 49 | // The following codes are based on: 50 | // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code 51 | 52 | static let normalClosure = WSCloseCode(1000) 53 | static let goingAway = WSCloseCode(1001, reason: "Going Away") 54 | static let protocolError = WSCloseCode(1002, reason: "Protocol Error") 55 | static let unsupportedData = WSCloseCode(1003, reason: "Unsupported Data") 56 | static let noStatusReceived = WSCloseCode(1005, reason: "No Status Received") 57 | static let abnormalClosure = WSCloseCode(1006, reason: "Abnormal Closure") 58 | static let invalidFramePayload = WSCloseCode(1007, reason: "Invalid Frame Payload") 59 | static let policyViolation = WSCloseCode(1008, reason: "Policy Violation") 60 | static let messageTooBig = WSCloseCode(1009, reason: "Message Too Big") 61 | static let mandatoryExtensionMissing = WSCloseCode(1010, reason: "Mandatory Extension Missing") 62 | static let internalServerError = WSCloseCode(1011, reason: "Internal Server Error") 63 | static let serviceRestart = WSCloseCode(1012, reason: "Service Restart") 64 | static let tryAgainLater = WSCloseCode(1013, reason: "Try Again Later") 65 | static let badGateway = WSCloseCode(1014, reason: "Bad Gateway") 66 | static let tlsHandshakeFailure = WSCloseCode(1015, reason: "TLS Handshake Failure") 67 | } 68 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHandler.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 14/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public protocol HTTPHandler: Sendable { 35 | func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse 36 | } 37 | 38 | public struct HTTPUnhandledError: LocalizedError { 39 | public let errorDescription: String? = "HTTPHandler can not handle the request." 40 | public init() { } 41 | } 42 | 43 | public extension HTTPHandler where Self == FileHTTPHandler { 44 | static func file(named: String, in bundle: Bundle = .main) -> FileHTTPHandler { 45 | FileHTTPHandler(named: named, in: bundle) 46 | } 47 | } 48 | 49 | public extension HTTPHandler where Self == DirectoryHTTPHandler { 50 | static func directory(for bundle: Bundle = .main, subPath: String = "", serverPath: String = "") -> DirectoryHTTPHandler { 51 | DirectoryHTTPHandler(bundle: bundle, subPath: subPath, serverPath: serverPath) 52 | } 53 | } 54 | 55 | public extension HTTPHandler where Self == RedirectHTTPHandler { 56 | static func redirect(to location: String) -> RedirectHTTPHandler { 57 | RedirectHTTPHandler(location: location) 58 | } 59 | 60 | static func redirect(via base: String, serverPath: String? = nil) -> RedirectHTTPHandler { 61 | RedirectHTTPHandler(base: base, serverPath: serverPath) 62 | } 63 | } 64 | 65 | public extension HTTPHandler where Self == ProxyHTTPHandler { 66 | static func proxy(via url: String) -> ProxyHTTPHandler { 67 | ProxyHTTPHandler(base: url) 68 | } 69 | } 70 | 71 | public extension HTTPHandler where Self == ClosureHTTPHandler { 72 | static func unhandled() -> ClosureHTTPHandler { 73 | ClosureHTTPHandler { _ in throw HTTPUnhandledError() } 74 | } 75 | } 76 | 77 | public extension HTTPHandler where Self == WebSocketHTTPHandler { 78 | static func webSocket(_ handler: some WSMessageHandler, frameSize: Int = 16384) -> WebSocketHTTPHandler { 79 | WebSocketHTTPHandler(handler: MessageFrameWSHandler(handler: handler, frameSize: frameSize)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /FlyingFox/XCTests/HTTPResponse+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPResponse+Mock.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 17/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import FlyingFox 33 | import FlyingSocks 34 | import Foundation 35 | 36 | extension HTTPResponse { 37 | 38 | static func make(version: HTTPVersion = .http11, 39 | statusCode: HTTPStatusCode = .ok, 40 | headers: [HTTPHeader: String] = [:], 41 | body: Data = Data()) -> Self { 42 | HTTPResponse(version: version, 43 | statusCode: statusCode, 44 | headers: headers, 45 | body: body) 46 | } 47 | 48 | static func makeChunked(version: HTTPVersion = .http11, 49 | statusCode: HTTPStatusCode = .ok, 50 | headers: [HTTPHeader: String] = [:], 51 | body: Data = Data(), 52 | chunkSize: Int = 5) -> Self { 53 | let consuming = ConsumingAsyncSequence(body) 54 | return HTTPResponse( 55 | version: version, 56 | statusCode: statusCode, 57 | headers: headers, 58 | body: HTTPBodySequence(from: consuming, suggestedBufferSize: chunkSize) 59 | ) 60 | } 61 | 62 | static func make(version: HTTPVersion = .http11, 63 | statusCode: HTTPStatusCode = .ok, 64 | headers: [HTTPHeader: String] = [:], 65 | body: HTTPBodySequence) -> Self { 66 | HTTPResponse(version: version, 67 | statusCode: statusCode, 68 | headers: headers, 69 | body: body) 70 | } 71 | 72 | static func make(headers: [HTTPHeader: String] = [:], 73 | webSocket handler: some WSHandler) -> Self { 74 | HTTPResponse(headers: headers, 75 | webSocket: handler) 76 | } 77 | 78 | var bodyString: String? { 79 | get async throws { 80 | try await String(data: bodyData, encoding: .utf8) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /FlyingFox/Tests/JSON/JSONBodyPatternTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONBodyPatternTests.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 15/08/2024. 6 | // Copyright © 2024 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import FlyingFox 33 | import Foundation 34 | import Testing 35 | 36 | struct JSONBodyPatternTests { 37 | 38 | @Test 39 | func pattern_MatchesJSONPath() async throws { 40 | // given 41 | let pattern = JSONBodyPattern { $0["$.name"] == "fish" } 42 | 43 | // when then 44 | #expect(pattern.evaluate(json: #"{"name": "fish"}"#)) 45 | #expect(pattern.evaluate(json: #"{"id": 5, "name": "fish"}"#)) 46 | #expect(!pattern.evaluate(json: #"{"name": "chips"}"#)) 47 | #expect(!pattern.evaluate(json: #"{}"#)) 48 | #expect(!pattern.evaluate(json: #""#)) 49 | } 50 | 51 | @Test 52 | func route_MatchesJSONPath() async throws { 53 | // given 54 | let route = HTTPRoute( 55 | "POST /fish", 56 | jsonBody: { $0["$.food"] == "chips" } 57 | ) 58 | 59 | // when 60 | var result = await route ~= .make(path: "fish", bodyJSON: #"{"food": "chips"}"#) 61 | 62 | // then 63 | #expect(result) 64 | 65 | // when 66 | result = await route ~= .make(path: "fish", bodyJSON: #"{"food": "shrimp"}"#) 67 | 68 | // then 69 | #expect(!result) 70 | } 71 | } 72 | 73 | private extension JSONBodyPattern { 74 | 75 | func evaluate(json: String) -> Bool { 76 | self.evaluate(Data(json.utf8)) 77 | } 78 | } 79 | 80 | private extension HTTPRequest { 81 | static func make(method: HTTPMethod = .POST, 82 | version: HTTPVersion = .http11, 83 | path: String = "/", 84 | query: [QueryItem] = [], 85 | headers: [HTTPHeader: String] = [:], 86 | bodyJSON: String) -> Self { 87 | HTTPRequest(method: method, 88 | version: version, 89 | path: path, 90 | query: query, 91 | headers: headers, 92 | body: Data(bodyJSON.utf8)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /FlyingSocks/Sources/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 19/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | public protocol Logging: Sendable { 33 | func logDebug(_ debug: @autoclosure () -> String) 34 | func logInfo(_ info: @autoclosure () -> String) 35 | func logWarning(_ warning: @autoclosure () -> String) 36 | func logError(_ error: @autoclosure () -> String) 37 | func logCritical(_ critical: @autoclosure () -> String) 38 | } 39 | 40 | public struct PrintLogger: Logging { 41 | 42 | let category: String 43 | 44 | public init(category: String) { 45 | self.category = category 46 | } 47 | 48 | public func logDebug(_ debug: @autoclosure () -> String) { 49 | Swift.print("[\(category)] debug: \(debug())") 50 | } 51 | 52 | public func logInfo(_ info: @autoclosure () -> String) { 53 | Swift.print("[\(category)] info: \(info())") 54 | } 55 | 56 | public func logWarning(_ warning: @autoclosure () -> String) { 57 | Swift.print("[\(category)] warning: \(warning())") 58 | } 59 | 60 | public func logError(_ error: @autoclosure () -> String) { 61 | Swift.print("[\(category)] error: \(error())") 62 | } 63 | 64 | public func logCritical(_ critical: @autoclosure () -> String) { 65 | Swift.print("[\(category)] critical: \(critical())") 66 | } 67 | } 68 | 69 | public struct DisabledLogger: Logging { 70 | 71 | public func logDebug(_ debug: @autoclosure () -> String) { } 72 | 73 | public func logInfo(_ info: @autoclosure () -> String) { } 74 | 75 | public func logWarning(_ warning: @autoclosure () -> String) { } 76 | 77 | public func logError(_ error: @autoclosure () -> String) { } 78 | 79 | public func logCritical(_ critical: @autoclosure () -> String) { } 80 | } 81 | 82 | public extension Logging where Self == PrintLogger { 83 | 84 | static func print(category: String) -> Self { 85 | PrintLogger(category: category) 86 | } 87 | } 88 | 89 | public extension Logging where Self == DisabledLogger { 90 | 91 | static var disabled: Self { 92 | DisabledLogger() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /FlyingFox/Sources/JSON/JSONPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONPath.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 29/05/2023. 6 | // Copyright © 2023 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Foundation 33 | 34 | public struct JSONPath { 35 | 36 | var components: [Component] 37 | 38 | enum Component: Equatable { 39 | case field(String) 40 | case array(Int) 41 | } 42 | 43 | init(components: [Component]) { 44 | self.components = components 45 | } 46 | 47 | public init(parsing path: String) throws { 48 | self.components = try Self.parseComponents(from: path) 49 | } 50 | 51 | private struct Error: LocalizedError { 52 | var errorDescription: String? 53 | 54 | init(_ description: String) { 55 | self.errorDescription = description 56 | } 57 | } 58 | } 59 | 60 | extension JSONPath { 61 | 62 | static func parseComponents(from path: String) throws -> [Component] { 63 | var scanner = Scanner(string: path) 64 | guard scanner.scanString("$") != nil else { 65 | throw Error("Expected $") 66 | } 67 | 68 | var comps = [Component]() 69 | while let comp = try scanComponent(from: &scanner) { 70 | comps.append(comp) 71 | } 72 | return comps 73 | } 74 | 75 | static func scanComponent(from scanner: inout Scanner) throws -> Component? { 76 | if scanner.scanString(".") != nil { 77 | guard let name = scanner.scanUpToCharacters(from: CharacterSet(charactersIn: ".[")) else { 78 | throw Error("Expected field name") 79 | } 80 | return .field(name) 81 | } else if scanner.scanString("[") != nil { 82 | guard let index = scanner.scanCharacters(from: CharacterSet(charactersIn: "0123456789")) else { 83 | throw Error("Expected index") 84 | } 85 | guard scanner.scanString("]") != nil else { 86 | throw Error("Expected ]") 87 | } 88 | return .array(Int(index)!) 89 | } 90 | guard scanner.isAtEnd else { 91 | throw Error("Expected end") 92 | } 93 | return nil 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeader.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 13/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | public struct HTTPHeader: Sendable, RawRepresentable, Hashable { 33 | public var rawValue: String 34 | 35 | public init(rawValue: String) { 36 | self.rawValue = rawValue 37 | } 38 | 39 | public init(_ rawValue: String) { 40 | self.init(rawValue: rawValue) 41 | } 42 | 43 | public func hash(into hasher: inout Hasher) { 44 | rawValue.lowercased().hash(into: &hasher) 45 | } 46 | 47 | public static func == (lhs: HTTPHeader, rhs: HTTPHeader) -> Bool { 48 | lhs.rawValue.caseInsensitiveCompare(rhs.rawValue) == .orderedSame 49 | } 50 | } 51 | 52 | public extension HTTPHeader { 53 | static let acceptRanges = HTTPHeader("Accept-Ranges") 54 | static let authorization = HTTPHeader("Authorization") 55 | static let cookie = HTTPHeader("Cookie") 56 | static let connection = HTTPHeader("Connection") 57 | static let contentDisposition = HTTPHeader("Content-Disposition") 58 | static let contentEncoding = HTTPHeader("Content-Encoding") 59 | static let contentLength = HTTPHeader("Content-Length") 60 | static let contentRange = HTTPHeader("Content-Range") 61 | static let contentType = HTTPHeader("Content-Type") 62 | static let date = HTTPHeader("Date") 63 | static let eTag = HTTPHeader("ETag") 64 | static let host = HTTPHeader("Host") 65 | static let location = HTTPHeader("Location") 66 | static let range = HTTPHeader("Range") 67 | static let setCookie = HTTPHeader("Set-Cookie") 68 | static let transferEncoding = HTTPHeader("Transfer-Encoding") 69 | static let upgrade = HTTPHeader("Upgrade") 70 | static let webSocketAccept = HTTPHeader("Sec-WebSocket-Accept") 71 | static let webSocketKey = HTTPHeader("Sec-WebSocket-Key") 72 | static let webSocketVersion = HTTPHeader("Sec-WebSocket-Version") 73 | static let xForwardedFor = HTTPHeader("X-Forwarded-For") 74 | } 75 | -------------------------------------------------------------------------------- /FlyingFox/Sources/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // FlyingFox 4 | // 5 | // Created by Simon Whitty on 17/02/2022. 6 | // Copyright © 2022 Simon Whitty. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/FlyingFox 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | public struct HTTPMethod: Sendable, RawRepresentable, Hashable, ExpressibleByStringLiteral { 33 | public var rawValue: String 34 | 35 | public init(rawValue: String) { 36 | self.rawValue = rawValue 37 | } 38 | 39 | public init(_ rawValue: String) { 40 | self.init(rawValue: rawValue.uppercased()) 41 | } 42 | 43 | public init(stringLiteral value: String) { 44 | self.init(rawValue: value.uppercased()) 45 | } 46 | } 47 | 48 | public extension HTTPMethod { 49 | func hash(into hasher: inout Hasher) { 50 | rawValue.uppercased().hash(into: &hasher) 51 | } 52 | 53 | static func == (lhs: Self, rhs: Self) -> Bool { 54 | return lhs.rawValue.uppercased() == rhs.rawValue.uppercased() 55 | } 56 | } 57 | 58 | public extension HTTPMethod { 59 | internal static let sortedMethods = [ 60 | HTTPMethod.GET, 61 | .POST, 62 | .PUT, 63 | .DELETE, 64 | .PATCH, 65 | .HEAD, 66 | .OPTIONS, 67 | .CONNECT, 68 | .TRACE 69 | ] 70 | 71 | static let allMethods = Set(HTTPMethod.sortedMethods) 72 | 73 | static let GET = HTTPMethod("GET") 74 | static let POST = HTTPMethod("POST") 75 | static let PUT = HTTPMethod("PUT") 76 | static let DELETE = HTTPMethod("DELETE") 77 | static let PATCH = HTTPMethod("PATCH") 78 | static let HEAD = HTTPMethod("HEAD") 79 | static let OPTIONS = HTTPMethod("OPTIONS") 80 | static let CONNECT = HTTPMethod("CONNECT") 81 | static let TRACE = HTTPMethod("TRACE") 82 | } 83 | 84 | public extension Set { 85 | 86 | /// Comma delimited string of methods, sorted to ensure default methods appear first. 87 | var stringValue: String { 88 | var sortedMethods = HTTPMethod 89 | .sortedMethods 90 | .filter { contains($0) } 91 | 92 | sortedMethods.append(contentsOf: self.filter { !HTTPMethod.allMethods.contains($0) }) 93 | return sortedMethods.map(\.rawValue).joined(separator: ",") 94 | } 95 | } 96 | --------------------------------------------------------------------------------