Skip to content

Commit

Permalink
Merge pull request #2 from christophhagen/1-add-oneof-protobuf-compat…
Browse files Browse the repository at this point in the history
…ibility

Add Oneof Protobuf Compatibility
  • Loading branch information
christophhagen authored Jul 19, 2022
2 parents f308212 + be1a834 commit 23d56cc
Show file tree
Hide file tree
Showing 32 changed files with 687 additions and 34 deletions.
103 changes: 76 additions & 27 deletions ProtobufSupport.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,27 @@ The assignment of integer keys follow the same [rules](https://developers.google

There are several [scalar types](https://developers.google.com/protocol-buffers/docs/proto3#scalar) defined for Protocol Buffers, which are the basic building blocks of messages. `BinaryCodable` provides Swift equivalents for each of them:

| Protobuf primitive | Swift equivalent | Comment |
| :-- | :-- | :-- |
`double` | `Double` Always 8 byte
`float` `Float` | Always 4 byte
`int32` | `Int32` Uses variable-length encoding
`int64` `Int64` Uses variable-length encoding
`uint32` | `UInt32` Uses variable-length encoding
`uint64` `UInt64` Uses variable-length encoding
`sint32` `SignedInteger<Int32>` | Uses ZigZag encoding, see [`SignedInteger` wrapper](#signed-integers)
`sint64` `SignedInteger<Int64>` | Uses ZigZag encoding, see [`SignedInteger` wrapper](#signed-integers)
`fixed32` `FixedSize<UInt32>` See [`FixedSize` wrapper](#fixed-size-integers)
`fixed64` `FixedSize<UInt64>` See [`FixedSize` wrapper](#fixed-size-integers)
`sfixed32` `FixedSize<Int32>` See [`FixedSize` wrapper](#fixed-size-integers)
`sfixed64` `FixedSize<Int64>` See [`FixedSize` wrapper](#fixed-size-integers)
`bool` `Bool` Always 1 byte
`string` `String` | Encoded using UTF-8
`bytes` | `Data` | Encoded as-is
`message` `struct` Nested messages are also supported.
`repeated``Array` Scalar values must always be `packed` (the proto3 default)
`enum` `Enum` See [Enums](#enums)
`oneof` | N/A | No `Codable` equivalent available
| Protobuf primitive | Swift equivalent | Comment |
| :----------------- | :--------------------- | :-------------------------------------------------------------------- |
| `double` | `Double` | Always 8 byte |
| `float` | `Float` | Always 4 byte |
| `int32` | `Int32` | Uses variable-length encoding |
| `int64` | `Int64` | Uses variable-length encoding |
| `uint32` | `UInt32` | Uses variable-length encoding |
| `uint64` | `UInt64` | Uses variable-length encoding |
| `sint32` | `SignedInteger<Int32>` | Uses ZigZag encoding, see [`SignedInteger` wrapper](#signed-integers) |
| `sint64` | `SignedInteger<Int64>` | Uses ZigZag encoding, see [`SignedInteger` wrapper](#signed-integers) |
| `fixed32` | `FixedSize<UInt32>` | See [`FixedSize` wrapper](#fixed-size-integers) |
| `fixed64` | `FixedSize<UInt64>` | See [`FixedSize` wrapper](#fixed-size-integers) |
| `sfixed32` | `FixedSize<Int32>` | See [`FixedSize` wrapper](#fixed-size-integers) |
| `sfixed64` | `FixedSize<Int64>` | See [`FixedSize` wrapper](#fixed-size-integers) |
| `bool` | `Bool` | Always 1 byte |
| `string` | `String` | Encoded using UTF-8 |
| `bytes` | `Data` | Encoded as-is |
| `message` | `struct` | Nested messages are also supported. |
| `repeated` | `Array` | Scalar values must always be `packed` (the proto3 default) |
| `enum` | `Enum` | See [Enums](#enums) |
| `oneof` | `Enum` | See [OneOf Definition](#oneof) |

The Swift types `Int8`, `UInt8`, `Int16`, and `UInt16` are **not** supported, and will result in an error.

Expand All @@ -120,12 +120,12 @@ Note: `Int` and `UInt` values are always encoded as 64-bit numbers, despite the

The Protocol Buffer format provides several different encoding strategies for integers to minimize the binary size depending on the encoded values. By default, all integers are encoded using [Base 128 Varints](https://developers.google.com/protocol-buffers/docs/encoding#varints), but this can be changed using Swift `PropertyWrappers`. The following encoding options exist:

| Swift type | [Varint encoding](https://developers.google.com/protocol-buffers/docs/encoding#varints) | [ZigZag Encoding](https://developers.google.com/protocol-buffers/docs/encoding#signed-ints) | [Fixed-size encoding](https://developers.google.com/protocol-buffers/docs/encoding#non-varint_numbers) |
| :-- | :-- | :-- | :-- |
`Int32` `Int32` `SignedInteger<Int32>` `FixedSize<Int32>`
`Int64` `Int64` `SignedInteger<Int64>` `FixedSize<Int64>`
`UInt32` `UInt32` | - | `FixedSize<UInt32>`
`UInt64` `UInt64` | - | `FixedSize<UInt64>`
| Swift type | [Varint encoding](https://developers.google.com/protocol-buffers/docs/encoding#varints) | [ZigZag Encoding](https://developers.google.com/protocol-buffers/docs/encoding#signed-ints) | [Fixed-size encoding](https://developers.google.com/protocol-buffers/docs/encoding#non-varint_numbers) |
| :--------- | :-------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------- |
| `Int32` | `Int32` | `SignedInteger<Int32>` | `FixedSize<Int32>` |
| `Int64` | `Int64` | `SignedInteger<Int64>` | `FixedSize<Int64>` |
| `UInt32` | `UInt32` | - | `FixedSize<UInt32>` |
| `UInt64` | `UInt64` | - | `FixedSize<UInt64>` |

#### Fixed size integers

Expand Down Expand Up @@ -203,3 +203,52 @@ struct SearchRequest: Codable {
```

It should be noted that protobuf enums require a default key `0`.

### Oneof

The protobuf feature [Oneof](https://developers.google.com/protocol-buffers/docs/proto3#oneof) can also be supported using a special enum definition. Given the protobuf definition (from [here](https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md#oneof-fields)):

```proto
syntax = "proto3";
message ExampleOneOf {
int32 field1 = 1;
oneof alternatives {
int64 id = 2;
string name = 3;
}
}
```

The corresponding Swift definition would be:

```swift
struct ExampleOneOf: Codable {

let field1: Int32

// The oneof field
let alternatives: Alternatives

// The OneOf definition
enum Alternatives: Codable, ProtobufOneOf {
case id(Int64)
case name(String)

// Field values, must not overlap with `ExampleOneOf.CodingKeys`
enum CodingKeys: Int, CodingKey {
case id = 2
case name = 3
}
}

enum CodingKeys: Int, CodingKey {
case field1 = 1
// The field id of the Oneof field is not used
case alternatives = 123456
}
}
```

Note that the `Alternatives` enum must conform to `ProtobufOneOf`, which changes the encoding to create compatibility with the Protobuf binary format.

**Important** The `ProtobufOneOf` protocol must not be applied to any other types, or encoding/decoding will fail.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation

/**
A keyed decoder specifically to decode the associated value within a `ProtobufOneOf` type.
The decoder receives the data associated with the enum case from `OneOfKeyedDecoder`,
and allows exactly one associated value key (`_0`) to be decoded from the data.
The decoder allows only one call to `decode(_:forKey:)`, all other access will fail.
*/
final class OneOfAssociatedValuesDecoder<Key>: AbstractDecodingNode, KeyedDecodingContainerProtocol where Key: CodingKey {

private let data: Data

init(data: Data, path: [CodingKey], info: UserInfo) {
self.data = data
super.init(path: path, info: info)
}

var allKeys: [Key] {
[Key(stringValue: "_0")!]
}

func contains(_ key: Key) -> Bool {
return true
}

func decodeNil(forKey key: Key) throws -> Bool {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `decodeNil(forKey:)` while decoding associated value for a `ProtobufOneOf` type")
}

func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
if let _ = type as? DecodablePrimitive.Type {
if let ProtoType = type as? ProtobufDecodable.Type {
if !data.isEmpty {
return try ProtoType.init(fromProtobuf: data) as! T
} else {
return ProtoType.zero as! T
}
}
throw ProtobufDecodingError.unsupported(type: type)
} else if type is AnyDictionary.Type {
let node = ProtoDictDecodingNode(data: data, path: codingPath, info: userInfo)
return try T.init(from: node)
}
let node = ProtoDecodingNode(data: data, path: codingPath, info: userInfo)
return try T.init(from: node)
}

func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `nestedContainer(keyedBy:forKey:)` while decoding associated value for a `ProtobufOneOf` type")
}

func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `nestedUnkeyedContainer(forKey:)` while decoding associated value for a `ProtobufOneOf` type")
}

func superDecoder() throws -> Decoder {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `superDecoder()` while decoding associated value for a `ProtobufOneOf` type")
}

func superDecoder(forKey key: Key) throws -> Decoder {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `superDecoder(forKey:)` while decoding associated value for a `ProtobufOneOf` type")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

typealias KeyedDataCallback = ((CodingKey) -> Data?)

/**
A decoding node specifically to decode protobuf `OneOf` structures.
The node receives all data from the parent container, and hands it to a `OneOfKeyedDecoder`,
which then decodes the keyed values within the enum.
Attempts to access other containers (`keyed` or `single`) will throw an error.
*/
final class OneOfDecodingNode: AbstractDecodingNode, Decoder {

private let content: [DecodingKey: Data]

init(content: [DecodingKey: Data], path: [CodingKey], info: UserInfo) {
self.content = content
super.init(path: path, info: info)
}

func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
let container = OneOfKeyedDecoder<Key>(content: content, path: codingPath, info: userInfo)
return KeyedDecodingContainer(container)
}

func unkeyedContainer() throws -> UnkeyedDecodingContainer {
throw ProtobufDecodingError.invalidAccess(
"Attempt to access unkeyedContainer() for a type conforming to ProtobufOneOf")
}

func singleValueContainer() throws -> SingleValueDecodingContainer {
throw ProtobufDecodingError.invalidAccess(
"Attempt to access singleValueContainer() for a type conforming to ProtobufOneOf")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Foundation

/**
A keyed decoder specifically to decode an enum of type `ProtobufOneOf`.
The decoder only allows a call to `nestedContainer(keyedBy:forKey:)`
to decode the associated value of the `OneOf`, all other access will fail with an error.
The decoder receives all key-value pairs from the parent node (passed through `OneOfDecodingNode`),
in order to select the appropriate data required to decode the enum (`OneOf` types share the field values with the enclosing message).
*/
final class OneOfKeyedDecoder<Key>: AbstractDecodingNode, KeyedDecodingContainerProtocol where Key: CodingKey {

private let content: [DecodingKey: Data]

init(content: [DecodingKey: Data], path: [CodingKey], info: UserInfo) {
self.content = content
super.init(path: path, info: info)
}

var allKeys: [Key] {
content.keys.compactMap { key in
switch key {
case .intKey(let value):
return Key(intValue: value)
case .stringKey(let value):
return Key(stringValue: value)
}
}
}

func contains(_ key: Key) -> Bool {
content.keys.contains { $0.isEqual(to: key) }
}

func decodeNil(forKey key: Key) throws -> Bool {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `decodeNil(forKey:)` while decoding `ProtobufOneOf` enum")
}

func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `decode(_,forKey)` while decoding `ProtobufOneOf` enum")
}

func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
let data = content.first(where: { $0.key.isEqual(to: key) })?.value ?? Data()
let container = OneOfAssociatedValuesDecoder<NestedKey>(data: data, path: codingPath, info: userInfo)
return KeyedDecodingContainer(container)
}

func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `nestedUnkeyedContainer(forKey:)` while decoding `ProtobufOneOf` enum")
}

func superDecoder() throws -> Decoder {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `superDecoder()` while decoding `ProtobufOneOf` enum")
}

func superDecoder(forKey key: Key) throws -> Decoder {
throw ProtobufDecodingError.invalidAccess(
"Unexpected call to `superDecoder(forKey:)` while decoding `ProtobufOneOf` enum")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ class ProtoKeyedDecoder<Key>: AbstractDecodingNode, KeyedDecodingContainerProtoc
content.keys.contains { $0.isEqual(to: key) }
}


private func getDataIfAvailable(forKey key: CodingKey) -> Data? {
content.first(where: { $0.key.isEqual(to: key) })?.value
}
Expand All @@ -65,6 +64,10 @@ class ProtoKeyedDecoder<Key>: AbstractDecodingNode, KeyedDecodingContainerProtoc
}

func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
if type is ProtobufOneOf.Type {
let node = OneOfDecodingNode(content: content, path: codingPath, info: userInfo)
return try T.init(from: node)
}
let data = getDataIfAvailable(forKey: key)
if let _ = type as? DecodablePrimitive.Type {
if let ProtoType = type as? ProtobufDecodable.Type {
Expand Down
9 changes: 5 additions & 4 deletions Sources/BinaryCodable/Encoding/EncodedPrimitive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ struct EncodedPrimitive: EncodingContainer {
let dataType: DataType

let data: Data

let isEmpty: Bool

init(primitive: EncodablePrimitive) throws {
self.dataType = primitive.dataType
self.data = try primitive.data()
self.isEmpty = false
}

init(protobuf: EncodablePrimitive, excludeDefaults: Bool = false) throws {
Expand All @@ -20,13 +23,11 @@ struct EncodedPrimitive: EncodingContainer {
}
if excludeDefaults && value.isZero {
self.data = .empty
self.isEmpty = true
} else {
self.data = try value.protobufData()
self.isEmpty = false
}
self.dataType = protobuf.dataType
}

var isEmpty: Bool {
data.isEmpty
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ final class KeyedProtoEncoder<Key>: AbstractProtoNode, KeyedEncodingContainerPro
}

func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
if value is ProtobufOneOf {
// TODO: Create oneof protobuf definition
throw ProtobufEncodingError.unsupportedType("Oneof definition")
}
if let primitive = value as? EncodablePrimitive {
guard let protoPrimitive = primitive as? ProtobufEncodable else {
throw ProtobufEncodingError.unsupported(type: primitive)
Expand Down
Loading

0 comments on commit 23d56cc

Please sign in to comment.