Skip to content

Commit

Permalink
Revise PostgresDataEncoder implementation to better handle various Co…
Browse files Browse the repository at this point in the history
…dable conformances (#234)

Revise PostgresDataEncoder implementation to better handle various Codable conformances (no more fatalError()s!)
  • Loading branch information
gwynne authored Oct 28, 2022
1 parent df9146a commit 35deea5
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 134 deletions.
240 changes: 107 additions & 133 deletions Sources/PostgresKit/PostgresDataEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,148 +13,122 @@ public final class PostgresDataEncoder {
if let custom = value as? PostgresDataConvertible, let data = custom.postgresData {
return data
} else {
let context = _Context()
try value.encode(to: _Encoder(context: context))
if let value = context.value {
return value
} else if let array = context.array {
let elementType = array.first?.type ?? .jsonb
assert(array.filter { $0.type != elementType }.isEmpty, "Array does not contain all: \(elementType)")
return PostgresData(array: array, elementType: elementType)
} else {
return try PostgresData(jsonb: self.json.encode(_Wrapper(value)))
let encoder = _Encoder(parent: self)
do {
try value.encode(to: encoder)
switch encoder.value {
case .invalid: throw _Encoder.AssociativeValueSentinel() // this is usually "nothing was encoded at all", not an associative value, but the desired action is the same
case .scalar(let scalar): return scalar
case .indexed(let indexed):
let elementType = indexed.contents.first?.type ?? .jsonb
assert(indexed.contents.allSatisfy { $0.type == elementType }, "Type \(type(of: value)) was encoded as a heterogenous array; this is unsupported.")
return PostgresData(array: indexed.contents, elementType: elementType)
}
} catch is _Encoder.AssociativeValueSentinel {
#if swift(<5.7)
struct _Wrapper: Encodable {
let encodable: Encodable
init(_ encodable: Encodable) { self.encodable = encodable }
func encode(to encoder: Encoder) throws { try self.encodable.encode(to: encoder) }
}
return try PostgresData(jsonb: self.json.encode(_Wrapper(value))) // Swift <5.7 will complain that "Encodable does not conform to Encodable" without the wrapper
#else
return try PostgresData(jsonb: self.json.encode(value))
#endif
}
}
}

final class _Context {
var value: PostgresData?
var array: [PostgresData]?

init() { }
}

struct _Encoder: Encoder {
var userInfo: [CodingUserInfoKey : Any] {
[:]
}
var codingPath: [CodingKey] {
[]
private final class _Encoder: Encoder {
struct AssociativeValueSentinel: Error {}
enum Value {
final class RefArray<T> { var contents: [T] = [] }
case invalid, indexed(RefArray<PostgresData>), scalar(PostgresData)

var isValid: Bool { if case .invalid = self { return false }; return true }
mutating func requestIndexed(for encoder: _Encoder) {
switch self {
case .scalar(_): preconditionFailure("Invalid request for both single-value and unkeyed containers from the same encoder.")
case .invalid: self = .indexed(.init()) // no existing value, make new array
case .indexed(_): break // existing array, adopt it for appending (support for superEncoder())
}
}
mutating func storeScalar(_ scalar: PostgresData) {
switch self {
case .indexed(_), .scalar(_): preconditionFailure("Invalid request for multiple containers from the same encoder.")
case .invalid: self = .scalar(scalar) // no existing value, store the incoming
}
}
var indexedCount: Int {
switch self {
case .invalid, .scalar(_): preconditionFailure("Internal error in encoder (requested indexed count from non-indexed state)")
case .indexed(let ref): return ref.contents.count
}
}
mutating func addToIndexed(_ scalar: PostgresData) {
switch self {
case .invalid, .scalar(_): preconditionFailure("Internal error in encoder (attempted store to indexed in non-indexed state)")
case .indexed(let ref): ref.contents.append(scalar)
}
}
}
let context: _Context

func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key>
where Key : CodingKey
{
.init(_KeyedEncoder<Key>())

var userInfo: [CodingUserInfoKey : Any] { [:] }; var codingPath: [CodingKey] { [] }
var parent: PostgresDataEncoder, value: Value

init(parent: PostgresDataEncoder, value: Value = .invalid) { (self.parent, self.value) = (parent, value) }
func container<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> {
precondition(!self.value.isValid, "Requested multiple containers from the same encoder.")
return .init(_FailingKeyedContainer())
}

func unkeyedContainer() -> UnkeyedEncodingContainer {
self.context.array = []
return _UnkeyedEncoder(context: self.context)
self.value.requestIndexed(for: self)
return _UnkeyedValueContainer(encoder: self)
}

func singleValueContainer() -> SingleValueEncodingContainer {
_ValueEncoder(context: self.context)
}
}

struct _UnkeyedEncoder: UnkeyedEncodingContainer {
var codingPath: [CodingKey] {
[]
}
var count: Int {
0
}

var context: _Context

func encodeNil() throws {
self.context.array!.append(.null)
}

func encode<T>(_ value: T) throws where T : Encodable {
try self.context.array!.append(PostgresDataEncoder().encode(value))
}

func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type
) -> KeyedEncodingContainer<NestedKey>
where NestedKey : CodingKey
{
fatalError()
}

func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
fatalError()
}

func superEncoder() -> Encoder {
fatalError()
}
}

struct _KeyedEncoder<Key>: KeyedEncodingContainerProtocol
where Key: CodingKey
{
var codingPath: [CodingKey] {
[]
}

func encodeNil(forKey key: Key) throws {
// do nothing
}

func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
// do nothing
}

func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey>
where NestedKey : CodingKey
{
fatalError()
}

func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {

fatalError()
}

func superEncoder() -> Encoder {
fatalError()
}

func superEncoder(forKey key: Key) -> Encoder {
fatalError()
}
}


struct _ValueEncoder: SingleValueEncodingContainer {
var codingPath: [CodingKey] {
[]
}
let context: _Context

func encodeNil() throws {
self.context.value = .null
}

func encode<T>(_ value: T) throws where T : Encodable {
self.context.value = try PostgresDataEncoder().encode(value)
}
}

struct _Wrapper: Encodable {
let encodable: Encodable
init(_ encodable: Encodable) {
self.encodable = encodable
}
func encode(to encoder: Encoder) throws {
try self.encodable.encode(to: encoder)
precondition(!self.value.isValid, "Requested multiple containers from the same encoder.")
return _SingleValueContainer(encoder: self)
}

struct _UnkeyedValueContainer: UnkeyedEncodingContainer {
let encoder: _Encoder; var codingPath: [CodingKey] { self.encoder.codingPath }
var count: Int { self.encoder.value.indexedCount }
mutating func encodeNil() throws { self.encoder.value.addToIndexed(.null) }
mutating func encode<T: Encodable>(_ value: T) throws { self.encoder.value.addToIndexed(try self.encoder.parent.encode(value)) }
mutating func nestedContainer<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> { self.superEncoder().container(keyedBy: K.self) }
mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { self.superEncoder().unkeyedContainer() }
mutating func superEncoder() -> Encoder { _Encoder(parent: self.encoder.parent, value: self.encoder.value) } // NOT the same as self.encoder
}

struct _SingleValueContainer: SingleValueEncodingContainer {
let encoder: _Encoder; var codingPath: [CodingKey] { self.encoder.codingPath }
func encodeNil() throws { self.encoder.value.storeScalar(.null) }
func encode<T: Encodable>(_ value: T) throws { self.encoder.value.storeScalar(try self.encoder.parent.encode(value)) }
}

/// This pair of types is only necessary because we can't directly throw an error from various Encoder and
/// encoding container methods. We define duplicate types rather than the old implementation's use of a
/// no-action keyed container because it can save a significant amount of time otherwise spent uselessly calling
/// nested methods in some cases.
struct _TaintedEncoder: Encoder, UnkeyedEncodingContainer, SingleValueEncodingContainer {
var userInfo: [CodingUserInfoKey : Any] { [:] }; var codingPath: [CodingKey] { [] }; var count: Int { 0 }
func container<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> { .init(_FailingKeyedContainer()) }
func nestedContainer<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> { .init(_FailingKeyedContainer()) }
func unkeyedContainer() -> UnkeyedEncodingContainer { self }
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { self }
func singleValueContainer() -> SingleValueEncodingContainer { self }
func superEncoder() -> Encoder { self }
func encodeNil() throws { throw AssociativeValueSentinel() }
func encode<T: Encodable>(_: T) throws { throw AssociativeValueSentinel() }
}
struct _FailingKeyedContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
var codingPath: [CodingKey] { [] }
func encodeNil(forKey: K) throws { throw AssociativeValueSentinel() }
func encode<T: Encodable>(_: T, forKey: K) throws { throw AssociativeValueSentinel() }
func nestedContainer<NK: CodingKey>(keyedBy: NK.Type, forKey: K) -> KeyedEncodingContainer<NK> { .init(_FailingKeyedContainer<NK>()) }
func nestedUnkeyedContainer(forKey: K) -> UnkeyedEncodingContainer { _TaintedEncoder() }
func superEncoder() -> Encoder { _TaintedEncoder() }
func superEncoder(forKey: K) -> Encoder { _TaintedEncoder() }
}
}
}
55 changes: 54 additions & 1 deletion Tests/PostgresKitTests/PostgresKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,61 @@ class PostgresKitTests: XCTestCase {
defer { try! connection.close().wait() }
try SQLBenchmarker(on: connection.sql()).testEnum()
}

/// Tests dealing with encoding of values whose `encode(to:)` implementation calls one of the `superEncoder()`
/// methods (most notably the implementation of `Codable` for Fluent's `Fields`, which we can't directly test
/// at this layer).
func testValuesThatUseSuperEncoder() throws {
struct UnusualType: Codable {
var prop1: String, prop2: [Bool], prop3: [[Bool]]

// This is intentionally contrived - Fluent's implementation does Codable this roundabout way as a
// workaround for the interaction of property wrappers with optional properties; it serves no purpose
// here other than to demonstrate that the encoder supports it.
private enum CodingKeys: String, CodingKey { case prop1, prop2, prop3 }
init(prop1: String, prop2: [Bool], prop3: [[Bool]]) { (self.prop1, self.prop2, self.prop3) = (prop1, prop2, prop3) }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.prop1 = try .init(from: container.superDecoder(forKey: .prop1))
var acontainer = try container.nestedUnkeyedContainer(forKey: .prop2), ongoing: [Bool] = []
while !acontainer.isAtEnd { ongoing.append(try Bool.init(from: acontainer.superDecoder())) }
self.prop2 = ongoing
var bcontainer = try container.nestedUnkeyedContainer(forKey: .prop3), bongoing: [[Bool]] = []
while !bcontainer.isAtEnd {
var ccontainer = try bcontainer.nestedUnkeyedContainer(), congoing: [Bool] = []
while !ccontainer.isAtEnd { congoing.append(try Bool.init(from: ccontainer.superDecoder())) }
bongoing.append(congoing)
}
self.prop3 = bongoing
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try self.prop1.encode(to: container.superEncoder(forKey: .prop1))
var acontainer = container.nestedUnkeyedContainer(forKey: .prop2)
for val in self.prop2 { try val.encode(to: acontainer.superEncoder()) }
var bcontainer = container.nestedUnkeyedContainer(forKey: .prop3)
for arr in self.prop3 {
var ccontainer = bcontainer.nestedUnkeyedContainer()
for val in arr { try val.encode(to: ccontainer.superEncoder()) }
}
}
}

let instance = UnusualType(prop1: "hello", prop2: [true, false, false, true], prop3: [[true, true], [false], [true], []])
let encoded1 = try PostgresDataEncoder().encode(instance)
let encoded2 = try PostgresDataEncoder().encode([instance, instance])

XCTAssertEqual(encoded1.type, .jsonb)
XCTAssertEqual(encoded2.type, .jsonbArray)

let decoded1 = try PostgresDataDecoder().decode(UnusualType.self, from: encoded1)
let decoded2 = try PostgresDataDecoder().decode([UnusualType].self, from: encoded2)

XCTAssertEqual(decoded1.prop3, instance.prop3)
XCTAssertEqual(decoded2.count, 2)
}

var eventLoop: EventLoop { self.eventLoopGroup.next() }
var eventLoop: EventLoop { self.eventLoopGroup.any() }
var eventLoopGroup: EventLoopGroup!

override func setUpWithError() throws {
Expand Down

0 comments on commit 35deea5

Please sign in to comment.