Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSONDecode and NIOFoundationCompat #641

Merged
merged 8 commits into from
Jan 7, 2025
9 changes: 6 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 7 additions & 7 deletions Sources/Hummingbird/Codable/JSON/JSONCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -46,6 +46,6 @@ extension JSONDecoder: RequestDecoder {
/// - context: Request context
public func decode<T: Decodable>(_ 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)
}
}
172 changes: 172 additions & 0 deletions Sources/Hummingbird/Utils/ByteBuffer+foundation.swift
Original file line number Diff line number Diff line change
@@ -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<T: Decodable>(
_ 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)
Joannis marked this conversation as resolved.
Show resolved Hide resolved
}
}

// 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<T: Decodable>(_ 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)!
}

}
2 changes: 1 addition & 1 deletion Tests/HummingbirdRouterTests/MiddlewareTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/HummingbirdTests/ApplicationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/HummingbirdTests/DateCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions Tests/HummingbirdTests/HummingBirdJSONTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, "[email protected]")
XCTAssertEqual(user.age, 25)
Expand Down
2 changes: 1 addition & 1 deletion Tests/HummingbirdTests/MiddlewareTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading