Skip to content
Permalink
Browse files

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.
  • Loading branch information...
weissi committed Oct 17, 2019
1 parent e102aa9 commit 8dd62cb0686a8ad6027181f2c15d39e9f44b5f56
@@ -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
}

0 comments on commit 8dd62cb

Please sign in to comment.
You can’t perform that action at this time.