Update the CBOREncodable protocol to require that a toCBOR function be

implemented. The `encode` function can then rely on a default implementation
that will call `toCBOR()` and then encode the resulting `CBOR` object.
This commit is contained in:
Hamilton Chapman
2022-06-28 12:11:01 +01:00
parent 8aeb9fe6a5
commit d2de0ba4e6
4 changed files with 263 additions and 22 deletions
+107 -22
View File
@@ -3,7 +3,17 @@ import Foundation
#endif
public protocol CBOREncodable {
/// Optional function that can potentially serve as an opportunity to optimize encoding.
func encode(options: CBOROptions) -> [UInt8]
/// Required function that returns the appropriate `CBOR` variant for a `CBOREncodable`-conforming value.
func toCBOR(options: CBOROptions) -> CBOR
}
extension CBOREncodable {
func encode(options: CBOROptions) -> [UInt8] {
self.toCBOR(options: options).encode(options: options)
}
}
extension CBOR: CBOREncodable {
@@ -30,45 +40,49 @@ extension CBOR: CBOREncodable {
case .break: return CBOR.encodeBreak()
}
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return self
}
}
extension Int: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
if (self < 0) {
return CBOR.encodeNegativeInt(Int64(self))
} else {
return CBOR.encodeVarUInt(UInt64(self))
}
return Int64(self).encode(options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return Int64(self).toCBOR(options: options)
}
}
extension Int8: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
if (self < 0) {
return CBOR.encodeNegativeInt(Int64(self))
} else {
return CBOR.encodeUInt8(UInt8(self))
}
return Int64(self).encode(options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return Int64(self).toCBOR(options: options)
}
}
extension Int16: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
if (self < 0) {
return CBOR.encodeNegativeInt(Int64(self))
} else {
return CBOR.encodeUInt16(UInt16(self))
}
return Int64(self).encode(options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return Int64(self).toCBOR(options: options)
}
}
extension Int32: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
if (self < 0) {
return CBOR.encodeNegativeInt(Int64(self))
} else {
return CBOR.encodeUInt32(UInt32(self))
}
return Int64(self).encode(options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return Int64(self).toCBOR(options: options)
}
}
@@ -77,14 +91,26 @@ extension Int64: CBOREncodable {
if (self < 0) {
return CBOR.encodeNegativeInt(self)
} else {
return CBOR.encodeUInt64(UInt64(self))
return CBOR.encodeVarUInt(UInt64(self))
}
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
if self < 0 {
return CBOR.negativeInt(~UInt64(bitPattern: self))
} else {
return CBOR.unsignedInt(UInt64(self))
}
}
}
extension UInt: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeVarUInt(UInt64(self))
return UInt64(self).encode(options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return UInt64(self).toCBOR(options: options)
}
}
@@ -92,60 +118,100 @@ extension UInt8: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeUInt8(self)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return UInt64(self).toCBOR(options: options)
}
}
extension UInt16: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeUInt16(self)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return UInt64(self).toCBOR(options: options)
}
}
extension UInt32: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeUInt32(self)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return UInt64(self).toCBOR(options: options)
}
}
extension UInt64: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeUInt64(self)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.unsignedInt(self)
}
}
extension String: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeString(self, options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.utf8String(self)
}
}
extension Float: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeFloat(self)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.float(self)
}
}
extension Double: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeDouble(self)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.double(self)
}
}
extension Bool: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeBool(self)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.boolean(self)
}
}
extension Array where Element: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeArray(self, options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.array(self.map { $0.toCBOR(options: options) })
}
}
extension Dictionary where Key: CBOREncodable, Value: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeMap(self, options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.map(Dictionary<CBOR, CBOR>(uniqueKeysWithValues: self.map { ($0.key.toCBOR(options: options), $0.value.toCBOR(options: options)) }))
}
}
extension Optional where Wrapped: CBOREncodable {
@@ -155,12 +221,23 @@ extension Optional where Wrapped: CBOREncodable {
case .none: return CBOR.encodeNull()
}
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
switch self {
case .some(let wrapped): return wrapped.toCBOR(options: options)
case .none: return CBOR.null
}
}
}
extension NSNull: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeNull()
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.null
}
}
#if canImport(Foundation)
@@ -168,11 +245,19 @@ extension Date: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeDate(self, options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.date(self)
}
}
extension Data: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeByteString(self.map{ $0 }, options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR.byteString(self.map { $0 })
}
}
#endif
+86
View File
@@ -297,6 +297,14 @@ extension CBOR {
switch any {
case is Int:
return (any as! Int).encode()
case is Int8:
return (any as! Int8).encode()
case is Int16:
return (any as! Int16).encode()
case is Int32:
return (any as! Int32).encode()
case is Int64:
return (any as! Int64).encode()
case is UInt:
return (any as! UInt).encode()
case is UInt8:
@@ -356,6 +364,84 @@ extension CBOR {
}
}
internal static func cborFromAny(_ any: Any?, options: CBOROptions = CBOROptions()) throws -> CBOR {
switch any {
case is Int:
return cborFromInt64(Int64(any as! Int))
case is Int8:
return cborFromInt64(Int64(any as! Int8))
case is Int16:
return cborFromInt64(Int64(any as! Int16))
case is Int32:
return cborFromInt64(Int64(any as! Int32))
case is Int64:
return cborFromInt64(any as! Int64)
case is UInt:
return CBOR.unsignedInt(UInt64(any as! UInt))
case is UInt8:
return CBOR.unsignedInt(UInt64(any as! UInt8))
case is UInt16:
return CBOR.unsignedInt(UInt64(any as! UInt16))
case is UInt32:
return CBOR.unsignedInt(UInt64(any as! UInt32))
case is UInt64:
return CBOR.unsignedInt(any as! UInt64)
case is String:
return CBOR.utf8String(any as! String)
case is Float:
return CBOR.float(any as! Float)
case is Double:
return CBOR.double(any as! Double)
case is Bool:
return CBOR.boolean(any as! Bool)
case is [UInt8]:
return CBOR.byteString(any as! [UInt8])
#if canImport(Foundation)
case is Data:
return CBOR.byteString((any as! Data).map { $0 })
case is Date:
return CBOR.date(any as! Date)
case is NSNull:
return CBOR.null
#endif
case is [Any?]:
let anyArr = any as! [Any?]
return try CBOR.array(anyArr.map { try cborFromAny($0) })
case is [String: Any?]:
let anyMap = any as! [String: Any?]
return try CBOR.map(Dictionary(
uniqueKeysWithValues: anyMap.map { try (cborFromAny($0.key), cborFromAny($0.value)) }
))
case is Void:
return CBOR.undefined
case nil:
return CBOR.null
default:
if let encodable = any as? CBOREncodable {
return encodable.toCBOR(options: options)
} else if let encodable = any as? Codable {
// This is very much a slow path - we fully encode and then
// decode the value to get it as a `CBOR`
let encoder = CodableCBOREncoder()
encoder.setOptions(options.toCodableEncoderOptions())
let encoded = try [UInt8](encoder.encode(encodable))
guard let decoded = try CBOR.decode(encoded) else {
throw CBOREncoderError.invalidType
}
return decoded
}
throw CBOREncoderError.invalidType
}
}
private static func cborFromInt64(_ i: Int64) -> CBOR {
if i < 0 {
return CBOR.negativeInt(~UInt64(bitPattern: i))
} else {
return CBOR.unsignedInt(UInt64(i))
}
}
private static func encodeMap<A: CBOREncodable>(_ map: [A: Any?], into res: inout [UInt8], options: CBOROptions = CBOROptions()) throws {
if options.forbidNonStringMapKeys {
try ensureStringKey(A.self)
+66
View File
@@ -0,0 +1,66 @@
import XCTest
@testable import SwiftCBOR
class CBOREncodableTests: XCTestCase {
func testToCBOR() {
XCTAssertEqual(CBOR.unsignedInt(0), 0.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(1), 1.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(20), 20.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64(Int8.max)), Int8.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(127, Int8.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64(Int16.max)), Int16.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(32_767, Int16.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64(Int32.max)), Int32.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(2_147_483_647, Int32.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64(Int64.max)), Int64.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(9_223_372_036_854_775_807, Int64.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64(UInt8.max)), UInt8.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(255, UInt8.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64(UInt16.max)), UInt16.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(65_535, UInt16.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64(UInt32.max)), UInt32.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(4_294_967_295, UInt32.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.unsignedInt(UInt64.max), UInt64.max.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int8.min))), Int8.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(-128, Int8.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int16.min))), Int16.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(-32_768, Int16.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int32.min))), Int32.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(-2_147_483_648, Int32.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.negativeInt(~UInt64(bitPattern: Int64(Int64.min))), Int64.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(-9_223_372_036_854_775_808, Int64.min.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.double(Double.greatestFiniteMagnitude), Double.greatestFiniteMagnitude.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.double(Double.leastNonzeroMagnitude), Double.leastNonzeroMagnitude.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.double(Double.pi), Double.pi.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.double(0.123456789), 0.123456789.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.float(Float.greatestFiniteMagnitude), Float.greatestFiniteMagnitude.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.float(Float.leastNonzeroMagnitude), Float.leastNonzeroMagnitude.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.float(Float.pi), Float.pi.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.float(0.123456789), Float(0.123456789).toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.boolean(true), true.toCBOR(options: CBOROptions()))
XCTAssertEqual(true, true.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.boolean(false), false.toCBOR(options: CBOROptions()))
XCTAssertEqual(false, false.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.utf8String("test"), "test".toCBOR(options: CBOROptions()))
XCTAssertEqual("test", "test".toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.null, Optional<String>.none.toCBOR(options: CBOROptions()))
XCTAssertEqual(nil, Optional<String>.none.toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.byteString([1, 2, 3]), Data([1, 2, 3]).toCBOR(options: CBOROptions()))
XCTAssertEqual(CBOR.array([CBOR.unsignedInt(1), CBOR.unsignedInt(2)]), [1, 2].toCBOR(options: CBOROptions()))
XCTAssertEqual(
CBOR.map([CBOR.utf8String("a"): CBOR.unsignedInt(1), CBOR.utf8String("b"): CBOR.unsignedInt(2)]),
["a": 1, "b": 2].toCBOR(options: CBOROptions())
)
}
}
+4
View File
@@ -250,6 +250,10 @@ class CBOREncoderTests: XCTestCase {
]
return cborWrapper.encode(options: options)
}
public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
return CBOR(dictionaryLiteral:("x", CBOR(integerLiteral: self.x)), ("y", .utf8String(self.y)))
}
}
let encoded = MyStruct(x: 42, y: "words").encode()