diff --git a/Package.swift b/Package.swift index e2eeba86..1810bb32 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let swiftSettings: [SwiftSetting] = [.enableExperimentalFeature("StrictConcurren let package = Package( name: "hummingbird", - platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)], + platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17), .visionOS(.v1)], products: [ .library(name: "Hummingbird", targets: ["Hummingbird"]), .library(name: "HummingbirdCore", targets: ["HummingbirdCore"]), @@ -46,7 +46,6 @@ let package = Package( .product(name: "Metrics", package: "swift-metrics"), .product(name: "Tracing", package: "swift-distributed-tracing"), .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), ], swiftSettings: swiftSettings @@ -64,7 +63,11 @@ let package = Package( .product(name: "NIOHTTPTypesHTTP1", package: "swift-nio-extras"), .product(name: "NIOExtras", package: "swift-nio-extras"), .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), + .product( + name: "NIOTransportServices", + package: "swift-nio-transport-services", + condition: .when(platforms: [.macOS, .iOS, .tvOS, .visionOS]) + ), .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), ], swiftSettings: swiftSettings diff --git a/Sources/Hummingbird/Application.swift b/Sources/Hummingbird/Application.swift index c72d48f3..0bf00c68 100644 --- a/Sources/Hummingbird/Application.swift +++ b/Sources/Hummingbird/Application.swift @@ -17,7 +17,6 @@ import Logging import NIOCore import NIOHTTPTypes import NIOPosix -import NIOTransportServices import ServiceLifecycle import UnixSignals diff --git a/Sources/Hummingbird/Codable/JSON/JSONCoding.swift b/Sources/Hummingbird/Codable/JSON/JSONCoding.swift index a8777884..eb13bebb 100644 --- a/Sources/Hummingbird/Codable/JSON/JSONCoding.swift +++ b/Sources/Hummingbird/Codable/JSON/JSONCoding.swift @@ -12,11 +12,11 @@ // //===----------------------------------------------------------------------===// -import NIOFoundationCompat - -import struct Foundation.Date -import class Foundation.JSONDecoder -import class Foundation.JSONEncoder +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif extension JSONEncoder: ResponseEncoder { /// Extend JSONEncoder to support encoding `Response`'s. Sets body and header values @@ -26,7 +26,7 @@ extension JSONEncoder: ResponseEncoder { /// - context: Request context public func encode(_ value: some Encodable, from request: Request, context: some RequestContext) throws -> Response { let data = try self.encode(value) - let buffer = ByteBuffer(data: data) + let buffer = ByteBuffer(bytes: data) return Response( status: .ok, headers: .defaultHummingbirdHeaders( @@ -46,6 +46,6 @@ extension JSONDecoder: RequestDecoder { /// - context: Request context public func decode(_ type: T.Type, from request: Request, context: some RequestContext) async throws -> T { let buffer = try await request.body.collect(upTo: context.maxUploadSize) - return try self.decode(T.self, from: buffer) + return try self.decodeByteBuffer(T.self, from: buffer) } } diff --git a/Sources/Hummingbird/Utils/ByteBuffer+foundation.swift b/Sources/Hummingbird/Utils/ByteBuffer+foundation.swift new file mode 100644 index 00000000..3140f533 --- /dev/null +++ b/Sources/Hummingbird/Utils/ByteBuffer+foundation.swift @@ -0,0 +1,172 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// MARK: Data + +extension ByteBuffer { + /// Controls how bytes are transferred between `ByteBuffer` and other storage types. + @usableFromInline + package enum _ByteTransferStrategy: Sendable { + /// Force a copy of the bytes. + case copy + + /// Do not copy the bytes if at all possible. + case noCopy + + /// Use a heuristic to decide whether to copy the bytes or not. + case automatic + } + + /// Return `length` bytes starting at `index` and return the result as `Data`. This will not change the reader index. + /// The selected bytes must be readable or else `nil` will be returned. + /// + /// - parameters: + /// - index: The starting index of the bytes of interest into the `ByteBuffer` + /// - length: The number of bytes of interest + /// - byteTransferStrategy: Controls how to transfer the bytes. See `ByteTransferStrategy` for an explanation + /// of the options. + /// - returns: A `Data` value containing the bytes of interest or `nil` if the selected bytes are not readable. + @usableFromInline + package func _getData(at index0: Int, length: Int, byteTransferStrategy: _ByteTransferStrategy) -> Data? { + let index = index0 - self.readerIndex + guard index >= 0 && length >= 0 && index <= self.readableBytes - length else { + return nil + } + let doCopy: Bool + switch byteTransferStrategy { + case .copy: + doCopy = true + case .noCopy: + doCopy = false + case .automatic: + doCopy = length <= 256 * 1024 + } + + return self.withUnsafeReadableBytesWithStorageManagement { ptr, storageRef in + if doCopy { + return Data( + bytes: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)), + count: Int(length) + ) + } else { + _ = storageRef.retain() + return Data( + bytesNoCopy: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)), + count: Int(length), + deallocator: .custom { _, _ in storageRef.release() } + ) + } + } + } + + /// Read `length` bytes off this `ByteBuffer`, move the reader index forward by `length` bytes and return the result + /// as `Data`. + /// + /// - parameters: + /// - length: The number of bytes to be read from this `ByteBuffer`. + /// - byteTransferStrategy: Controls how to transfer the bytes. See `ByteTransferStrategy` for an explanation + /// of the options. + /// - returns: A `Data` value containing `length` bytes or `nil` if there aren't at least `length` bytes readable. + package mutating func _readData(length: Int, byteTransferStrategy: _ByteTransferStrategy) -> Data? { + guard + let result = self._getData(at: self.readerIndex, length: length, byteTransferStrategy: byteTransferStrategy) + else { + return nil + } + self.moveReaderIndex(forwardBy: length) + return result + } + + /// Attempts to decode the `length` bytes from `index` using the `JSONDecoder` `decoder` as `T`. + /// + /// - parameters: + /// - type: The type type that is attempted to be decoded. + /// - decoder: The `JSONDecoder` that is used for the decoding. + /// - index: The index of the first byte to decode. + /// - length: The number of bytes to decode. + /// - returns: The decoded value if successful or `nil` if there are not enough readable bytes available. + @inlinable + package func _getJSONDecodable( + _ type: T.Type, + decoder: JSONDecoder = JSONDecoder(), + at index: Int, + length: Int + ) throws -> T? { + guard let data = self._getData(at: index, length: length, byteTransferStrategy: .noCopy) else { + return nil + } + return try decoder.decode(T.self, from: data) + } +} + +// MARK: JSONDecoder + +extension JSONDecoder { + /// Returns a value of the type you specify, decoded from a JSON object inside the readable bytes of a `ByteBuffer`. + /// + /// If the `ByteBuffer` does not contain valid JSON, this method throws the + /// `DecodingError.dataCorrupted(_:)` error. If a value within the JSON + /// fails to decode, this method throws the corresponding error. + /// + /// - note: The provided `ByteBuffer` remains unchanged, neither the `readerIndex` nor the `writerIndex` will move. + /// If you would like the `readerIndex` to move, consider using `ByteBuffer.readJSONDecodable(_:length:)`. + /// + /// - parameters: + /// - type: The type of the value to decode from the supplied JSON object. + /// - buffer: The `ByteBuffer` that contains JSON object to decode. + /// - returns: The decoded object. + package func decodeByteBuffer(_ type: T.Type, from buffer: ByteBuffer) throws -> T { + try buffer._getJSONDecodable( + T.self, + decoder: self, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes + } +} + +// MARK: Data + +extension Data { + + /// Creates a `Data` from a given `ByteBuffer`. The entire readable portion of the buffer will be read. + /// - parameter buffer: The buffer to read. + @_disfavoredOverload + package init(buffer: ByteBuffer, byteTransferStrategy: ByteBuffer._ByteTransferStrategy = .automatic) { + var buffer = buffer + self = buffer._readData(length: buffer.readableBytes, byteTransferStrategy: byteTransferStrategy)! + } + +} diff --git a/Sources/HummingbirdTesting/LiveTestFramework.swift b/Sources/HummingbirdTesting/LiveTestFramework.swift index ae9c0d90..9f51a9c9 100644 --- a/Sources/HummingbirdTesting/LiveTestFramework.swift +++ b/Sources/HummingbirdTesting/LiveTestFramework.swift @@ -18,7 +18,6 @@ import HummingbirdCore import Logging import NIOCore import NIOPosix -import NIOTransportServices import ServiceLifecycle /// Test using a live server diff --git a/Tests/HummingbirdCoreTests/TLSTests.swift b/Tests/HummingbirdCoreTests/TLSTests.swift index 134dd439..50aec082 100644 --- a/Tests/HummingbirdCoreTests/TLSTests.swift +++ b/Tests/HummingbirdCoreTests/TLSTests.swift @@ -20,7 +20,6 @@ import NIOConcurrencyHelpers import NIOCore import NIOPosix import NIOSSL -import NIOTransportServices import XCTest final class HummingBirdTLSTests: XCTestCase { diff --git a/Tests/HummingbirdHTTP2Tests/HTTP2Tests.swift b/Tests/HummingbirdHTTP2Tests/HTTP2Tests.swift index a0fbed4e..b6ca7f33 100644 --- a/Tests/HummingbirdHTTP2Tests/HTTP2Tests.swift +++ b/Tests/HummingbirdHTTP2Tests/HTTP2Tests.swift @@ -22,7 +22,6 @@ import NIOHTTP1 import NIOHTTPTypes import NIOPosix import NIOSSL -import NIOTransportServices import XCTest final class HummingBirdHTTP2Tests: XCTestCase { diff --git a/Tests/HummingbirdRouterTests/MiddlewareTests.swift b/Tests/HummingbirdRouterTests/MiddlewareTests.swift index 173ab56c..fe129d64 100644 --- a/Tests/HummingbirdRouterTests/MiddlewareTests.swift +++ b/Tests/HummingbirdRouterTests/MiddlewareTests.swift @@ -123,7 +123,7 @@ final class MiddlewareTests: XCTestCase { try await app.test(.router) { client in try await client.execute(uri: "/hello", method: .get) { response in XCTAssertEqual(response.status, .notFound) - let error = try JSONDecoder().decode(ErrorMessage.self, from: response.body) + let error = try JSONDecoder().decodeByteBuffer(ErrorMessage.self, from: response.body) XCTAssertEqual(error.error.message, "Edited error") } } diff --git a/Tests/HummingbirdTests/ApplicationTests.swift b/Tests/HummingbirdTests/ApplicationTests.swift index bef779af..84045c65 100644 --- a/Tests/HummingbirdTests/ApplicationTests.swift +++ b/Tests/HummingbirdTests/ApplicationTests.swift @@ -212,7 +212,7 @@ final class ApplicationTests: XCTestCase { let app = Application(router: router) try await app.test(.router) { client in try await client.execute(uri: "/error", method: .get) { response in - let error = try JSONDecoder().decode(ErrorMessage.self, from: response.body) + let error = try JSONDecoder().decodeByteBuffer(ErrorMessage.self, from: response.body) XCTAssertEqual(error.error.message, "BAD!") } } @@ -829,7 +829,7 @@ final class ApplicationTests: XCTestCase { let response = try error.response(from: request, context: context) let writer = CollatedResponseWriter() _ = try await response.body.write(writer) - let format = try JSONDecoder().decode(HTTPErrorFormat.self, from: writer.collated.withLockedValue { $0 }) + let format = try JSONDecoder().decodeByteBuffer(HTTPErrorFormat.self, from: writer.collated.withLockedValue { $0 }) XCTAssertEqual(format.error.message, message) } } diff --git a/Tests/HummingbirdTests/DateCacheTests.swift b/Tests/HummingbirdTests/DateCacheTests.swift index b8511083..1d06cea2 100644 --- a/Tests/HummingbirdTests/DateCacheTests.swift +++ b/Tests/HummingbirdTests/DateCacheTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import Hummingbird -final class HummingbirdDateTests: XCTestCase { +final class DateTests: XCTestCase { func testRFC1123Renderer() { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") diff --git a/Tests/HummingbirdTests/HummingBirdJSONTests.swift b/Tests/HummingbirdTests/HummingBirdJSONTests.swift index a5680c9a..97d0ffd1 100644 --- a/Tests/HummingbirdTests/HummingBirdJSONTests.swift +++ b/Tests/HummingbirdTests/HummingBirdJSONTests.swift @@ -17,7 +17,7 @@ import HummingbirdTesting import Logging import XCTest -final class HummingbirdJSONTests: XCTestCase { +final class JSONCodingTests: XCTestCase { struct User: ResponseCodable { let name: String let email: String @@ -52,7 +52,7 @@ final class HummingbirdJSONTests: XCTestCase { let app = Application(responder: router.buildResponder()) try await app.test(.router) { client in try await client.execute(uri: "/user", method: .get) { response in - let user = try JSONDecoder().decode(User.self, from: response.body) + let user = try JSONDecoder().decodeByteBuffer(User.self, from: response.body) XCTAssertEqual(user.name, "John Smith") XCTAssertEqual(user.email, "john.smith@email.com") XCTAssertEqual(user.age, 25) diff --git a/Tests/HummingbirdTests/MiddlewareTests.swift b/Tests/HummingbirdTests/MiddlewareTests.swift index 72c34c71..947f001b 100644 --- a/Tests/HummingbirdTests/MiddlewareTests.swift +++ b/Tests/HummingbirdTests/MiddlewareTests.swift @@ -118,7 +118,7 @@ final class MiddlewareTests: XCTestCase { try await app.test(.router) { client in try await client.execute(uri: "/hello", method: .get) { response in XCTAssertEqual(response.status, .notFound) - let error = try JSONDecoder().decode(ErrorMessage.self, from: response.body) + let error = try JSONDecoder().decodeByteBuffer(ErrorMessage.self, from: response.body) XCTAssertEqual(error.error.message, "Edited error") } } diff --git a/Tests/HummingbirdTests/URLEncodedForm/Application+URLEncodedFormTests.swift b/Tests/HummingbirdTests/URLEncodedForm/Application+URLEncodedFormTests.swift index c3847ce2..90ce5011 100644 --- a/Tests/HummingbirdTests/URLEncodedForm/Application+URLEncodedFormTests.swift +++ b/Tests/HummingbirdTests/URLEncodedForm/Application+URLEncodedFormTests.swift @@ -19,7 +19,7 @@ import Logging import NIOCore import XCTest -final class HummingBirdURLEncodedTests: XCTestCase { +final class URLEncodedFormTests: XCTestCase { struct User: ResponseCodable { let name: String let email: String