ByteBuffer: add direct Codable support (#1153)
Motivation: So far, it has been harder than necessary to use Codable & ByteBuffer. These new APIs should simplify that and allow future optimisations. Modifications: Add new API to use `JSONEncoder` and `JSONDecoder` directly with `ByteBuffer`. Result: Easier Codable + ByteBuffer usage.
This commit is contained in:
parent
e102aa9ae9
commit
8dd62cb068
|
@ -75,6 +75,8 @@ var targets: [PackageDescription.Target] = [
|
|||
dependencies: ["NIO", "NIOWebSocket"]),
|
||||
.testTarget(name: "NIOTestUtilsTests",
|
||||
dependencies: ["NIOTestUtils"]),
|
||||
.testTarget(name: "NIOFoundationCompatTests",
|
||||
dependencies: ["NIO", "NIOFoundationCompat"]),
|
||||
]
|
||||
|
||||
let package = Package(
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftNIO open source project
|
||||
//
|
||||
// Copyright (c) 2019 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 NIO
|
||||
import Foundation
|
||||
|
||||
extension ByteBuffer {
|
||||
/// 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
|
||||
public 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) else {
|
||||
return nil
|
||||
}
|
||||
return try decoder.decode(T.self, from: data)
|
||||
}
|
||||
|
||||
/// Reads `length` bytes from this `ByteBuffer` and then attempts to decode them using the `JSONDecoder` `decoder`.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - type: The type type that is attempted to be decoded.
|
||||
/// - decoder: The `JSONDecoder` that is used for the decoding.
|
||||
/// - length: The number of bytes to decode.
|
||||
/// - returns: The decoded value is successful or `nil` if there are not enough readable bytes available.
|
||||
@inlinable
|
||||
public mutating func readJSONDecodable<T: Decodable>(_ type: T.Type,
|
||||
decoder: JSONDecoder = JSONDecoder(),
|
||||
length: Int) throws -> T? {
|
||||
guard let decoded = try self.getJSONDecodable(T.self, at: self.readerIndex, length: length) else {
|
||||
return nil
|
||||
}
|
||||
self.moveReaderIndex(forwardBy: length)
|
||||
return decoded
|
||||
}
|
||||
|
||||
/// Encodes `value` using the `JSONEncoder` `encoder` and set the resulting bytes into this `ByteBuffer` at the
|
||||
/// given `index`.
|
||||
///
|
||||
/// - note: The `writerIndex` remains unchanged.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - value: An `Encodable` value to encode.
|
||||
/// - encoder: The `JSONEncoder` to encode `value` with.
|
||||
/// - returns: The number of bytes written.
|
||||
@inlinable
|
||||
@discardableResult
|
||||
public mutating func setJSONEncodable<T: Encodable>(_ value: T,
|
||||
encoder: JSONEncoder = JSONEncoder(),
|
||||
at index: Int) throws -> Int {
|
||||
let data = try encoder.encode(value)
|
||||
return self.setBytes(data, at: index)
|
||||
}
|
||||
|
||||
/// Encodes `value` using the `JSONEncoder` `encoder` and writes the resulting bytes into this `ByteBuffer`.
|
||||
///
|
||||
/// If successful, this will move the writer index forward by the number of bytes written.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - value: An `Encodable` value to encode.
|
||||
/// - encoder: The `JSONEncoder` to encode `value` with.
|
||||
/// - returns: The number of bytes written.
|
||||
@inlinable
|
||||
@discardableResult
|
||||
public mutating func writeJSONEncodable<T: Encodable>(_ value: T,
|
||||
encoder: JSONEncoder = JSONEncoder()) throws -> Int {
|
||||
let result = try self.setJSONEncodable(value, encoder: encoder, at: self.writerIndex)
|
||||
self.moveWriterIndex(forwardBy: result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
public func decode<T: Decodable>(_ type: T.Type, from buffer: ByteBuffer) throws -> T {
|
||||
return try buffer.getJSONDecodable(T.self,
|
||||
at: buffer.readerIndex,
|
||||
length: buffer.readableBytes)! // must work, enough readable bytes
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONEncoder {
|
||||
/// Writes a JSON-encoded representation of the value you supply into the supplied `ByteBuffer`.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - value: The value to encode as JSON.
|
||||
/// - buffer: The `ByteBuffer` to encode into.
|
||||
public func encode<T: Encodable>(_ value: T,
|
||||
into buffer: inout ByteBuffer) throws {
|
||||
try buffer.writeJSONEncodable(value, encoder: self)
|
||||
}
|
||||
|
||||
/// Writes a JSON-encoded representation of the value you supply into a `ByteBuffer` that is freshly allocated.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - value: The value to encode as JSON.
|
||||
/// - allocator: The `ByteBufferAllocator` which is used to allocate the `ByteBuffer` to be returned.
|
||||
/// - returns: The `ByteBuffer` containing the encoded JSON.
|
||||
public func encodeAsByteBuffer<T: Encodable>(_ value: T, allocator: ByteBufferAllocator) throws -> ByteBuffer {
|
||||
let data = try self.encode(value)
|
||||
var buffer = allocator.buffer(capacity: data.count)
|
||||
try buffer.writeJSONEncodable(value, encoder: self)
|
||||
return buffer
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import XCTest
|
|||
|
||||
#if os(Linux) || os(FreeBSD)
|
||||
@testable import NIOConcurrencyHelpersTests
|
||||
@testable import NIOFoundationCompatTests
|
||||
@testable import NIOHTTP1Tests
|
||||
@testable import NIOTLSTests
|
||||
@testable import NIOTestUtilsTests
|
||||
|
@ -47,6 +48,7 @@ import XCTest
|
|||
testCase(ChannelPipelineTest.allTests),
|
||||
testCase(ChannelTests.allTests),
|
||||
testCase(CircularBufferTests.allTests),
|
||||
testCase(CodableByteBufferTest.allTests),
|
||||
testCase(CustomChannelTests.allTests),
|
||||
testCase(DatagramChannelTests.allTests),
|
||||
testCase(EchoServerClientTest.allTests),
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftNIO open source project
|
||||
//
|
||||
// Copyright (c) 2017-2018 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Codable+ByteBufferTest+XCTest.swift
|
||||
//
|
||||
import XCTest
|
||||
|
||||
///
|
||||
/// NOTE: This file was generated by generate_linux_tests.rb
|
||||
///
|
||||
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
|
||||
///
|
||||
|
||||
extension CodableByteBufferTest {
|
||||
|
||||
static var allTests : [(String, (CodableByteBufferTest) -> () throws -> Void)] {
|
||||
return [
|
||||
("testSimpleDecode", testSimpleDecode),
|
||||
("testSimpleEncodeIntoBuffer", testSimpleEncodeIntoBuffer),
|
||||
("testSimpleEncodeToFreshByteBuffer", testSimpleEncodeToFreshByteBuffer),
|
||||
("testGetJSONDecodableFromBufferWorks", testGetJSONDecodableFromBufferWorks),
|
||||
("testGetJSONDecodableFromBufferFailsBecauseShort", testGetJSONDecodableFromBufferFailsBecauseShort),
|
||||
("testReadJSONDecodableFromBufferWorks", testReadJSONDecodableFromBufferWorks),
|
||||
("testReadJSONDecodableFromBufferFailsBecauseShort", testReadJSONDecodableFromBufferFailsBecauseShort),
|
||||
("testReadWriteJSONDecodableWorks", testReadWriteJSONDecodableWorks),
|
||||
("testGetSetJSONDecodableWorks", testGetSetJSONDecodableWorks),
|
||||
("testFailingReadsDoNotChangeReaderIndex", testFailingReadsDoNotChangeReaderIndex),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftNIO open source project
|
||||
//
|
||||
// Copyright (c) 2019 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 Foundation
|
||||
import XCTest
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
|
||||
class CodableByteBufferTest: XCTestCase {
|
||||
var buffer: ByteBuffer!
|
||||
var allocator: ByteBufferAllocator!
|
||||
var decoder: JSONDecoder!
|
||||
var encoder: JSONEncoder!
|
||||
|
||||
override func setUp() {
|
||||
self.allocator = ByteBufferAllocator()
|
||||
self.buffer = self.allocator.buffer(capacity: 1024)
|
||||
self.buffer.writeString(String(repeating: "A", count: 1024))
|
||||
self.buffer.moveReaderIndex(to: 129)
|
||||
self.buffer.moveWriterIndex(to: 129)
|
||||
self.decoder = JSONDecoder()
|
||||
self.encoder = JSONEncoder()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
self.encoder = nil
|
||||
self.decoder = nil
|
||||
self.buffer = nil
|
||||
self.allocator = nil
|
||||
}
|
||||
|
||||
func testSimpleDecode() {
|
||||
self.buffer.writeString(#"{"string": "hello", "int": 42}"#)
|
||||
var sAndI: StringAndInt?
|
||||
XCTAssertNoThrow(sAndI = try self.decoder.decode(StringAndInt.self, from: self.buffer))
|
||||
XCTAssertEqual(StringAndInt(string: "hello", int: 42), sAndI)
|
||||
}
|
||||
|
||||
func testSimpleEncodeIntoBuffer() {
|
||||
let expectedSandI = StringAndInt(string: "hello", int: 42)
|
||||
XCTAssertNoThrow(try self.encoder.encode(expectedSandI, into: &self.buffer))
|
||||
XCTAssertNoThrow(XCTAssertEqual(expectedSandI, try self.decoder.decode(StringAndInt.self, from: self.buffer)))
|
||||
}
|
||||
|
||||
func testSimpleEncodeToFreshByteBuffer() {
|
||||
let expectedSandI = StringAndInt(string: "hello", int: 42)
|
||||
var buffer = self.allocator.buffer(capacity: 0)
|
||||
XCTAssertNoThrow(buffer = try self.encoder.encodeAsByteBuffer(expectedSandI, allocator: self.allocator))
|
||||
XCTAssertNoThrow(XCTAssertEqual(expectedSandI, try self.decoder.decode(StringAndInt.self, from: buffer)))
|
||||
}
|
||||
|
||||
func testGetJSONDecodableFromBufferWorks() {
|
||||
self.buffer.writeString("GARBAGE {}!!? / GARBAGE")
|
||||
let beginIndex = self.buffer.writerIndex
|
||||
self.buffer.writeString(#"{"string": "hello", "int": 42}"#)
|
||||
let endIndex = self.buffer.writerIndex
|
||||
self.buffer.writeString("GARBAGE {}!!? / GARBAGE")
|
||||
|
||||
let expectedSandI = StringAndInt(string: "hello", int: 42)
|
||||
XCTAssertNoThrow(XCTAssertEqual(expectedSandI,
|
||||
try self.buffer.getJSONDecodable(StringAndInt.self,
|
||||
at: beginIndex,
|
||||
length: endIndex - beginIndex)))
|
||||
}
|
||||
|
||||
func testGetJSONDecodableFromBufferFailsBecauseShort() {
|
||||
self.buffer.writeString("GARBAGE {}!!? / GARBAGE")
|
||||
let beginIndex = self.buffer.writerIndex
|
||||
self.buffer.writeString(#"{"string": "hello", "int": 42}"#)
|
||||
let endIndex = self.buffer.writerIndex
|
||||
|
||||
XCTAssertThrowsError(try self.buffer.getJSONDecodable(StringAndInt.self,
|
||||
at: beginIndex,
|
||||
length: endIndex - beginIndex - 1)) { error in
|
||||
XCTAssert(error is DecodingError)
|
||||
}
|
||||
}
|
||||
|
||||
func testReadJSONDecodableFromBufferWorks() {
|
||||
let beginIndex = self.buffer.writerIndex
|
||||
self.buffer.writeString(#"{"string": "hello", "int": 42}"#)
|
||||
let endIndex = self.buffer.writerIndex
|
||||
self.buffer.writeString("GARBAGE {}!!? / GARBAGE")
|
||||
|
||||
let expectedSandI = StringAndInt(string: "hello", int: 42)
|
||||
XCTAssertNoThrow(XCTAssertEqual(expectedSandI,
|
||||
try self.buffer.readJSONDecodable(StringAndInt.self,
|
||||
length: endIndex - beginIndex)))
|
||||
}
|
||||
|
||||
func testReadJSONDecodableFromBufferFailsBecauseShort() {
|
||||
let beginIndex = self.buffer.writerIndex
|
||||
self.buffer.writeString(#"{"string": "hello", "int": 42}"#)
|
||||
let endIndex = self.buffer.writerIndex
|
||||
|
||||
XCTAssertThrowsError(try self.buffer.readJSONDecodable(StringAndInt.self,
|
||||
length: endIndex - beginIndex - 1)) { error in
|
||||
XCTAssert(error is DecodingError)
|
||||
}
|
||||
}
|
||||
|
||||
func testReadWriteJSONDecodableWorks() {
|
||||
let expectedSandI = StringAndInt(string: "hello", int: 42)
|
||||
self.buffer.writeString("hello")
|
||||
self.buffer.moveReaderIndex(forwardBy: 5)
|
||||
var writtenBytes: Int?
|
||||
XCTAssertNoThrow(writtenBytes = try self.buffer.writeJSONEncodable(expectedSandI))
|
||||
for _ in 0..<10 {
|
||||
XCTAssertNoThrow(try self.buffer.writeJSONEncodable(expectedSandI, encoder: JSONEncoder()))
|
||||
}
|
||||
for _ in 0..<11 {
|
||||
XCTAssertNoThrow(try self.buffer.readJSONDecodable(StringAndInt.self, length: writtenBytes ?? -1))
|
||||
}
|
||||
XCTAssertEqual(0, self.buffer.readableBytes)
|
||||
}
|
||||
|
||||
func testGetSetJSONDecodableWorks() {
|
||||
let expectedSandI = StringAndInt(string: "hello", int: 42)
|
||||
self.buffer.writeString(String(repeating: "{", count: 1000))
|
||||
var writtenBytes: Int?
|
||||
XCTAssertNoThrow(writtenBytes = try self.buffer.setJSONEncodable(expectedSandI,
|
||||
at: self.buffer.readerIndex + 123))
|
||||
XCTAssertNoThrow(try self.buffer.setJSONEncodable(expectedSandI,
|
||||
encoder: JSONEncoder(),
|
||||
at: self.buffer.readerIndex + 501))
|
||||
XCTAssertNoThrow(XCTAssertEqual(expectedSandI,
|
||||
try self.buffer.getJSONDecodable(StringAndInt.self,
|
||||
at: self.buffer.readerIndex + 123,
|
||||
length: writtenBytes ?? -1)))
|
||||
XCTAssertNoThrow(XCTAssertEqual(expectedSandI,
|
||||
try self.buffer.getJSONDecodable(StringAndInt.self,
|
||||
at: self.buffer.readerIndex + 501,
|
||||
length: writtenBytes ?? -1)))
|
||||
}
|
||||
|
||||
func testFailingReadsDoNotChangeReaderIndex() {
|
||||
let expectedSandI = StringAndInt(string: "hello", int: 42)
|
||||
var writtenBytes: Int?
|
||||
XCTAssertNoThrow(writtenBytes = try self.buffer.writeJSONEncodable(expectedSandI))
|
||||
for length in 0..<(writtenBytes ?? 0) {
|
||||
XCTAssertThrowsError(try self.buffer.readJSONDecodable(StringAndInt.self,
|
||||
length: length)) { error in
|
||||
XCTAssert(error is DecodingError)
|
||||
}
|
||||
}
|
||||
XCTAssertNoThrow(try self.buffer.readJSONDecodable(StringAndInt.self, length: writtenBytes ?? -1))
|
||||
}
|
||||
}
|
||||
|
||||
struct StringAndInt: Codable, Equatable {
|
||||
var string: String
|
||||
var int: Int
|
||||
}
|
Loading…
Reference in New Issue