From a6df9693af96b2f9679d4b555b894e6f464f2089 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Sat, 4 Dec 2021 12:07:09 +0100 Subject: [PATCH] Add LambdaRequestID --- NOTICE.txt | 9 + .../LambdaRequestID.swift | 381 ++++++++++++++++++ .../LambdaRequestIDTests.swift | 225 +++++++++++ 3 files changed, 615 insertions(+) create mode 100644 Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift create mode 100644 Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift diff --git a/NOTICE.txt b/NOTICE.txt index d82d8e67..9631ce9f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -33,3 +33,12 @@ This product contains a derivation various scripts from SwiftNIO. * https://www.apache.org/licenses/LICENSE-2.0 * HOMEPAGE: * https://github.com/apple/swift-nio + +--- + +This product contains a derivation of the swift-extras' 'swift-extras-uuid'. + + * LICENSE (MIT): + * https://github.com/swift-extras/swift-extras-uuid/blob/main/LICENSE + * HOMEPAGE: + * https://github.com/swift-extras/swift-extras-uuid diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift b/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift new file mode 100644 index 00000000..86178ff4 --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift @@ -0,0 +1,381 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// This is heavily inspired by: +// https://github.com/swift-extras/swift-extras-uuid + +struct LambdaRequestID { + typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + var uuid: uuid_t { + self._uuid + } + + static let null: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + + /// Creates a random [v4](https://tools.ietf.org/html/rfc4122#section-4.1.3) UUID. + init() { + self = Self.generateRandom() + } + + init?(uuidString: String) { + guard uuidString.utf8.count == 36 else { + return nil + } + + if let requestID = uuidString.utf8.withContiguousStorageIfAvailable({ ptr -> LambdaRequestID? in + let rawBufferPointer = UnsafeRawBufferPointer(ptr) + let requestID = Self.fromPointer(rawBufferPointer) + return requestID + }) { + if let requestID = requestID { + self = requestID + } else { + return nil + } + } else { + var newSwiftCopy = uuidString + newSwiftCopy.makeContiguousUTF8() + if let value = Self(uuidString: newSwiftCopy) { + self = value + } else { + return nil + } + } + } + + /// Creates a UUID from a `uuid_t`. + init(uuid: uuid_t) { + self._uuid = uuid + } + + private let _uuid: uuid_t + + /// Returns a string representation for the `LambdaRequestID`, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + var uuidString: String { + self.uppercased + } + + /// Returns a lowercase string representation for the `LambdaRequestID`, such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f" + var lowercased: String { + var bytes = self.toAsciiBytesOnStack(characters: Self.lowercaseLookup) + return withUnsafeBytes(of: &bytes) { + String(decoding: $0, as: Unicode.UTF8.self) + } + } + + /// Returns an uppercase string representation for the `LambdaRequestID`, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + var uppercased: String { + var bytes = self.toAsciiBytesOnStack(characters: Self.uppercaseLookup) + return withUnsafeBytes(of: &bytes) { + String(decoding: $0, as: Unicode.UTF8.self) + } + } + + /// thread safe secure random number generator. + private static var generator = SystemRandomNumberGenerator() + private static func generateRandom() -> Self { + var _uuid: uuid_t = LambdaRequestID.null + // https://tools.ietf.org/html/rfc4122#page-14 + // o Set all the other bits to randomly (or pseudo-randomly) chosen + // values. + withUnsafeMutableBytes(of: &_uuid) { ptr in + ptr.storeBytes(of: Self.generator.next(), toByteOffset: 0, as: UInt64.self) + ptr.storeBytes(of: Self.generator.next(), toByteOffset: 8, as: UInt64.self) + } + + // o Set the four most significant bits (bits 12 through 15) of the + // time_hi_and_version field to the 4-bit version number from + // Section 4.1.3. + _uuid.6 = (_uuid.6 & 0x0F) | 0x40 + + // o Set the two most significant bits (bits 6 and 7) of the + // clock_seq_hi_and_reserved to zero and one, respectively. + _uuid.8 = (_uuid.8 & 0x3F) | 0x80 + return LambdaRequestID(uuid: _uuid) + } +} + +// MARK: - Protocol extensions - + +extension LambdaRequestID: Equatable { + // sadly no auto conformance from the compiler + static func == (lhs: Self, rhs: Self) -> Bool { + lhs._uuid.0 == rhs._uuid.0 && + lhs._uuid.1 == rhs._uuid.1 && + lhs._uuid.2 == rhs._uuid.2 && + lhs._uuid.3 == rhs._uuid.3 && + lhs._uuid.4 == rhs._uuid.4 && + lhs._uuid.5 == rhs._uuid.5 && + lhs._uuid.6 == rhs._uuid.6 && + lhs._uuid.7 == rhs._uuid.7 && + lhs._uuid.8 == rhs._uuid.8 && + lhs._uuid.9 == rhs._uuid.9 && + lhs._uuid.10 == rhs._uuid.10 && + lhs._uuid.11 == rhs._uuid.11 && + lhs._uuid.12 == rhs._uuid.12 && + lhs._uuid.13 == rhs._uuid.13 && + lhs._uuid.14 == rhs._uuid.14 && + lhs._uuid.15 == rhs._uuid.15 + } +} + +extension LambdaRequestID: Hashable { + func hash(into hasher: inout Hasher) { + var value = self._uuid + withUnsafeBytes(of: &value) { ptr in + hasher.combine(bytes: ptr) + } + } +} + +extension LambdaRequestID: CustomStringConvertible { + var description: String { + self.uuidString + } +} + +extension LambdaRequestID: CustomDebugStringConvertible { + var debugDescription: String { + self.uuidString + } +} + +extension LambdaRequestID: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let uuidString = try container.decode(String.self) + + guard let uuid = LambdaRequestID.fromString(uuidString) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Attempted to decode UUID from invalid UUID string.") + } + + self = uuid + } +} + +extension LambdaRequestID: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.uuidString) + } +} + +// MARK: - Implementation details - + +extension LambdaRequestID { + fileprivate static let lowercaseLookup: [UInt8] = [ + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "a"), UInt8(ascii: "b"), + UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), + ] + + fileprivate static let uppercaseLookup: [UInt8] = [ + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "A"), UInt8(ascii: "B"), + UInt8(ascii: "C"), UInt8(ascii: "D"), UInt8(ascii: "E"), UInt8(ascii: "F"), + ] + + /// Use this type to create a backing store for a 8-4-4-4-12 UUID String on stack. + /// + /// Using this type we ensure to only have one allocation for creating a String even before Swift 5.3 and it can + /// also be used as an intermediary before copying the string bytes into a NIO `ByteBuffer`. + fileprivate typealias uuid_string_t = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 + ) + + fileprivate static let nullString: uuid_string_t = ( + 0, 0, 0, 0, 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ) + + fileprivate func toAsciiBytesOnStack(characters: [UInt8]) -> uuid_string_t { + var string: uuid_string_t = Self.nullString + // to get the best performance we access the lookup table's unsafe buffer pointer + // since the lookup table has 16 elements and we shift the byte values in such a way + // that the max value is 15 (last 4 bytes = 16 values). For this reason the lookups + // are safe and we don't need Swifts safety guards. + + characters.withUnsafeBufferPointer { lookup in + string.0 = lookup[Int(uuid.0 >> 4)] + string.1 = lookup[Int(uuid.0 & 0x0F)] + string.2 = lookup[Int(uuid.1 >> 4)] + string.3 = lookup[Int(uuid.1 & 0x0F)] + string.4 = lookup[Int(uuid.2 >> 4)] + string.5 = lookup[Int(uuid.2 & 0x0F)] + string.6 = lookup[Int(uuid.3 >> 4)] + string.7 = lookup[Int(uuid.3 & 0x0F)] + string.9 = lookup[Int(uuid.4 >> 4)] + string.10 = lookup[Int(uuid.4 & 0x0F)] + string.11 = lookup[Int(uuid.5 >> 4)] + string.12 = lookup[Int(uuid.5 & 0x0F)] + string.14 = lookup[Int(uuid.6 >> 4)] + string.15 = lookup[Int(uuid.6 & 0x0F)] + string.16 = lookup[Int(uuid.7 >> 4)] + string.17 = lookup[Int(uuid.7 & 0x0F)] + string.19 = lookup[Int(uuid.8 >> 4)] + string.20 = lookup[Int(uuid.8 & 0x0F)] + string.21 = lookup[Int(uuid.9 >> 4)] + string.22 = lookup[Int(uuid.9 & 0x0F)] + string.24 = lookup[Int(uuid.10 >> 4)] + string.25 = lookup[Int(uuid.10 & 0x0F)] + string.26 = lookup[Int(uuid.11 >> 4)] + string.27 = lookup[Int(uuid.11 & 0x0F)] + string.28 = lookup[Int(uuid.12 >> 4)] + string.29 = lookup[Int(uuid.12 & 0x0F)] + string.30 = lookup[Int(uuid.13 >> 4)] + string.31 = lookup[Int(uuid.13 & 0x0F)] + string.32 = lookup[Int(uuid.14 >> 4)] + string.33 = lookup[Int(uuid.14 & 0x0F)] + string.34 = lookup[Int(uuid.15 >> 4)] + string.35 = lookup[Int(uuid.15 & 0x0F)] + } + + return string + } + + static func fromString(_ string: String) -> LambdaRequestID? { + guard string.utf8.count == 36 else { + // invalid length + return nil + } + var string = string + return string.withUTF8 { + LambdaRequestID.fromPointer(.init($0)) + } + } +} + +extension LambdaRequestID { + static func fromPointer(_ ptr: UnsafeRawBufferPointer) -> LambdaRequestID? { + func uint4Value(from value: UInt8, valid: inout Bool) -> UInt8 { + switch value { + case UInt8(ascii: "0") ... UInt8(ascii: "9"): + return value &- UInt8(ascii: "0") + case UInt8(ascii: "a") ... UInt8(ascii: "f"): + return value &- UInt8(ascii: "a") &+ 10 + case UInt8(ascii: "A") ... UInt8(ascii: "F"): + return value &- UInt8(ascii: "A") &+ 10 + default: + valid = false + return 0 + } + } + + func dashCheck(from value: UInt8, valid: inout Bool) { + if value != UInt8(ascii: "-") { + valid = false + } + } + + precondition(ptr.count == 36) + var uuid = Self.null + var valid = true + uuid.0 = uint4Value(from: ptr[0], valid: &valid) &<< 4 &+ uint4Value(from: ptr[1], valid: &valid) + uuid.1 = uint4Value(from: ptr[2], valid: &valid) &<< 4 &+ uint4Value(from: ptr[3], valid: &valid) + uuid.2 = uint4Value(from: ptr[4], valid: &valid) &<< 4 &+ uint4Value(from: ptr[5], valid: &valid) + uuid.3 = uint4Value(from: ptr[6], valid: &valid) &<< 4 &+ uint4Value(from: ptr[7], valid: &valid) + dashCheck(from: ptr[8], valid: &valid) + uuid.4 = uint4Value(from: ptr[9], valid: &valid) &<< 4 &+ uint4Value(from: ptr[10], valid: &valid) + uuid.5 = uint4Value(from: ptr[11], valid: &valid) &<< 4 &+ uint4Value(from: ptr[12], valid: &valid) + dashCheck(from: ptr[13], valid: &valid) + uuid.6 = uint4Value(from: ptr[14], valid: &valid) &<< 4 &+ uint4Value(from: ptr[15], valid: &valid) + uuid.7 = uint4Value(from: ptr[16], valid: &valid) &<< 4 &+ uint4Value(from: ptr[17], valid: &valid) + dashCheck(from: ptr[18], valid: &valid) + uuid.8 = uint4Value(from: ptr[19], valid: &valid) &<< 4 &+ uint4Value(from: ptr[20], valid: &valid) + uuid.9 = uint4Value(from: ptr[21], valid: &valid) &<< 4 &+ uint4Value(from: ptr[22], valid: &valid) + dashCheck(from: ptr[23], valid: &valid) + uuid.10 = uint4Value(from: ptr[24], valid: &valid) &<< 4 &+ uint4Value(from: ptr[25], valid: &valid) + uuid.11 = uint4Value(from: ptr[26], valid: &valid) &<< 4 &+ uint4Value(from: ptr[27], valid: &valid) + uuid.12 = uint4Value(from: ptr[28], valid: &valid) &<< 4 &+ uint4Value(from: ptr[29], valid: &valid) + uuid.13 = uint4Value(from: ptr[30], valid: &valid) &<< 4 &+ uint4Value(from: ptr[31], valid: &valid) + uuid.14 = uint4Value(from: ptr[32], valid: &valid) &<< 4 &+ uint4Value(from: ptr[33], valid: &valid) + uuid.15 = uint4Value(from: ptr[34], valid: &valid) &<< 4 &+ uint4Value(from: ptr[35], valid: &valid) + + if valid { + return LambdaRequestID(uuid: uuid) + } + + return nil + } +} + +extension ByteBuffer { + func getRequestID(at index: Int) -> LambdaRequestID? { + guard let range = self.rangeWithinReadableBytes(index: index, length: 36) else { + return nil + } + return self.withUnsafeReadableBytes { ptr in + LambdaRequestID.fromPointer(UnsafeRawBufferPointer(fastRebase: ptr[range])) + } + } + + mutating func readRequestID() -> LambdaRequestID? { + guard let requestID = self.getRequestID(at: self.readerIndex) else { + return nil + } + self.moveReaderIndex(forwardBy: 36) + return requestID + } + + @discardableResult + mutating func setRequestID(_ requestID: LambdaRequestID, at index: Int) -> Int { + var localBytes = requestID.toAsciiBytesOnStack(characters: LambdaRequestID.lowercaseLookup) + return withUnsafeBytes(of: &localBytes) { + self.setBytes($0, at: index) + } + } + + mutating func writeRequestID(_ requestID: LambdaRequestID) -> Int { + let length = self.setRequestID(requestID, at: self.writerIndex) + self.moveWriterIndex(forwardBy: length) + return length + } + + // copy and pasted from NIOCore + func rangeWithinReadableBytes(index: Int, length: Int) -> Range? { + guard index >= self.readerIndex && length >= 0 else { + return nil + } + + // both these &-s are safe, they can't underflow because both left & right side are >= 0 (and index >= readerIndex) + let indexFromReaderIndex = index &- self.readerIndex + assert(indexFromReaderIndex >= 0) + guard indexFromReaderIndex <= self.readableBytes &- length else { + return nil + } + + let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above. + + // uncheckedBounds is safe because `length` is >= 0, so the lower bound will always be lower/equal to upper + return Range(uncheckedBounds: (lower: indexFromReaderIndex, upper: upperBound)) + } +} + +// copy and pasted from NIOCore +extension UnsafeRawBufferPointer { + init(fastRebase slice: Slice) { + let base = slice.base.baseAddress?.advanced(by: slice.startIndex) + self.init(start: base, count: slice.endIndex &- slice.startIndex) + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift new file mode 100644 index 00000000..7849fe09 --- /dev/null +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift @@ -0,0 +1,225 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaRuntimeCore +import NIOCore +import XCTest + +final class LambdaRequestIDTest: XCTestCase { + func testInitFromStringSuccess() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + var buffer = ByteBuffer(string: string) + + let requestID = buffer.readRequestID() + XCTAssertEqual(buffer.readerIndex, 36) + XCTAssertEqual(buffer.readableBytes, 0) + XCTAssertEqual(requestID?.uuidString, UUID(uuidString: string)?.uuidString) + XCTAssertEqual(requestID?.uppercased, string) + } + + func testInitFromLowercaseStringSuccess() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F".lowercased() + var originalBuffer = ByteBuffer(string: string) + + let requestID = originalBuffer.readRequestID() + XCTAssertEqual(originalBuffer.readerIndex, 36) + XCTAssertEqual(originalBuffer.readableBytes, 0) + XCTAssertEqual(requestID?.uuidString, UUID(uuidString: string)?.uuidString) + XCTAssertEqual(requestID?.lowercased, string) + + var newBuffer = ByteBuffer() + originalBuffer.moveReaderIndex(to: 0) + XCTAssertNoThrow(try newBuffer.writeRequestID(XCTUnwrap(requestID))) + XCTAssertEqual(newBuffer, originalBuffer) + } + + func testInitFromStringMissingCharacterAtEnd() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5" + var buffer = ByteBuffer(string: string) + + let readableBeforeRead = buffer.readableBytes + let requestID = buffer.readRequestID() + XCTAssertNil(requestID) + XCTAssertEqual(buffer.readerIndex, 0) + XCTAssertEqual(buffer.readableBytes, readableBeforeRead) + } + + func testInitFromStringInvalidCharacterAtEnd() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5H" + var buffer = ByteBuffer(string: string) + + let readableBeforeRead = buffer.readableBytes + let requestID = buffer.readRequestID() + XCTAssertNil(requestID) + XCTAssertEqual(buffer.readerIndex, 0) + XCTAssertEqual(buffer.readableBytes, readableBeforeRead) + } + + func testInitFromStringInvalidSeparatorCharacter() { + let invalid = [ + // with _ instead of - + "E621E1F8-C36C-495A-93FC_0C247A3E6E5F", + "E621E1F8-C36C-495A_93FC-0C247A3E6E5F", + "E621E1F8-C36C_495A-93FC-0C247A3E6E5F", + "E621E1F8_C36C-495A-93FC-0C247A3E6E5F", + + // with 0 instead of - + "E621E1F8-C36C-495A-93FC00C247A3E6E5F", + "E621E1F8-C36C-495A093FC-0C247A3E6E5F", + "E621E1F8-C36C0495A-93FC-0C247A3E6E5F", + "E621E1F80C36C-495A-93FC-0C247A3E6E5F", + ] + + for string in invalid { + var buffer = ByteBuffer(string: string) + + let readableBeforeRead = buffer.readableBytes + let requestID = buffer.readRequestID() + XCTAssertNil(requestID) + XCTAssertEqual(buffer.readerIndex, 0) + XCTAssertEqual(buffer.readableBytes, readableBeforeRead) + } + } + + func testInitFromNSStringSuccess() { + let nsString = NSMutableString(capacity: 16) + nsString.append("E621E1F8") + nsString.append("-") + nsString.append("C36C") + nsString.append("-") + nsString.append("495A") + nsString.append("-") + nsString.append("93FC") + nsString.append("-") + nsString.append("0C247A3E6E5F") + + // TODO: I would love to enforce that the nsstring is not contiguous + // here to enforce a special code path. I have no idea how to + // achieve this though at the moment + // XCTAssertFalse((nsString as String).isContiguousUTF8) + let requestID = LambdaRequestID(uuidString: nsString as String) + XCTAssertEqual(requestID?.uuidString, LambdaRequestID(uuidString: nsString as String)?.uuidString) + XCTAssertEqual(requestID?.uppercased, nsString as String) + } + + func testUnparse() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + let requestID = LambdaRequestID(uuidString: string) + XCTAssertEqual(string.lowercased(), requestID?.lowercased) + } + + func testDescription() { + let requestID = LambdaRequestID() + let fduuid = UUID(uuid: requestID.uuid) + + XCTAssertEqual(fduuid.description, requestID.description) + XCTAssertEqual(fduuid.debugDescription, requestID.debugDescription) + } + + func testFoundationInteropFromFoundation() { + let fduuid = UUID() + let requestID = LambdaRequestID(uuid: fduuid.uuid) + + XCTAssertEqual(fduuid.uuid.0, requestID.uuid.0) + XCTAssertEqual(fduuid.uuid.1, requestID.uuid.1) + XCTAssertEqual(fduuid.uuid.2, requestID.uuid.2) + XCTAssertEqual(fduuid.uuid.3, requestID.uuid.3) + XCTAssertEqual(fduuid.uuid.4, requestID.uuid.4) + XCTAssertEqual(fduuid.uuid.5, requestID.uuid.5) + XCTAssertEqual(fduuid.uuid.6, requestID.uuid.6) + XCTAssertEqual(fduuid.uuid.7, requestID.uuid.7) + XCTAssertEqual(fduuid.uuid.8, requestID.uuid.8) + XCTAssertEqual(fduuid.uuid.9, requestID.uuid.9) + XCTAssertEqual(fduuid.uuid.10, requestID.uuid.10) + XCTAssertEqual(fduuid.uuid.11, requestID.uuid.11) + XCTAssertEqual(fduuid.uuid.12, requestID.uuid.12) + XCTAssertEqual(fduuid.uuid.13, requestID.uuid.13) + XCTAssertEqual(fduuid.uuid.14, requestID.uuid.14) + XCTAssertEqual(fduuid.uuid.15, requestID.uuid.15) + } + + func testFoundationInteropToFoundation() { + let requestID = LambdaRequestID() + let fduuid = UUID(uuid: requestID.uuid) + + XCTAssertEqual(fduuid.uuid.0, requestID.uuid.0) + XCTAssertEqual(fduuid.uuid.1, requestID.uuid.1) + XCTAssertEqual(fduuid.uuid.2, requestID.uuid.2) + XCTAssertEqual(fduuid.uuid.3, requestID.uuid.3) + XCTAssertEqual(fduuid.uuid.4, requestID.uuid.4) + XCTAssertEqual(fduuid.uuid.5, requestID.uuid.5) + XCTAssertEqual(fduuid.uuid.6, requestID.uuid.6) + XCTAssertEqual(fduuid.uuid.7, requestID.uuid.7) + XCTAssertEqual(fduuid.uuid.8, requestID.uuid.8) + XCTAssertEqual(fduuid.uuid.9, requestID.uuid.9) + XCTAssertEqual(fduuid.uuid.10, requestID.uuid.10) + XCTAssertEqual(fduuid.uuid.11, requestID.uuid.11) + XCTAssertEqual(fduuid.uuid.12, requestID.uuid.12) + XCTAssertEqual(fduuid.uuid.13, requestID.uuid.13) + XCTAssertEqual(fduuid.uuid.14, requestID.uuid.14) + XCTAssertEqual(fduuid.uuid.15, requestID.uuid.15) + } + + func testHashing() { + let requestID = LambdaRequestID() + let fduuid = UUID(uuid: requestID.uuid) + XCTAssertEqual(fduuid.hashValue, requestID.hashValue) + + var _uuid = requestID.uuid + _uuid.0 = _uuid.0 > 0 ? _uuid.0 - 1 : 1 + XCTAssertNotEqual(UUID(uuid: _uuid).hashValue, requestID.hashValue) + } + + func testEncoding() { + struct Test: Codable { + let requestID: LambdaRequestID + } + let requestID = LambdaRequestID() + let test = Test(requestID: requestID) + + var data: Data? + XCTAssertNoThrow(data = try JSONEncoder().encode(test)) + XCTAssertEqual(try String(decoding: XCTUnwrap(data), as: Unicode.UTF8.self), #"{"requestID":"\#(requestID.uuidString)"}"#) + } + + func testDecodingSuccess() { + struct Test: Codable { + let requestID: LambdaRequestID + } + let requestID = LambdaRequestID() + let data = #"{"requestID":"\#(requestID.uuidString)"}"#.data(using: .utf8) + + var result: Test? + XCTAssertNoThrow(result = try JSONDecoder().decode(Test.self, from: XCTUnwrap(data))) + XCTAssertEqual(result?.requestID, requestID) + } + + func testDecodingFailure() { + struct Test: Codable { + let requestID: LambdaRequestID + } + let requestID = LambdaRequestID() + var requestIDString = requestID.uuidString + _ = requestIDString.removeLast() + let data = #"{"requestID":"\#(requestIDString)"}"#.data(using: .utf8) + + XCTAssertThrowsError(_ = try JSONDecoder().decode(Test.self, from: XCTUnwrap(data))) { error in + XCTAssertNotNil(error as? DecodingError) + } + } + + func testStructSize() { + XCTAssertEqual(MemoryLayout.size, 16) + } +}