Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise MySQLDataEncoder implementation to better handle various Codable conformances #313

Merged
merged 1 commit into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 55 additions & 137 deletions Sources/MySQLKit/MySQLDataEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,26 @@ public struct MySQLDataEncoder {
if let custom = value as? MySQLDataConvertible, let data = custom.mysqlData {
return data
} else {
let encoder = _Encoder()
try value.encode(to: encoder)
if let data = encoder.data {
return data
} else {
let encoder = _Encoder(parent: self)
do {
try value.encode(to: encoder)
if let value = encoder.value {
return value
} else {
throw _Encoder.NonScalarValueSentinel()
}
} catch is _Encoder.NonScalarValueSentinel {
var buffer = ByteBufferAllocator().buffer(capacity: 0)
try buffer.writeBytes(self.json.encode(_Wrapper(value)))
#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) }
}
try buffer.writeBytes(self.json.encode(_Wrapper(value))) // Swift <5.7 will complain that "Encodable does not conform to Encodable" without the wrapper
#else
try buffer.writeBytes(self.json.encode(value))
#endif
return MySQLData(
type: .string,
format: .text,
Expand All @@ -27,145 +40,50 @@ public struct MySQLDataEncoder {
}
}
}

private final class _Encoder: Encoder {
var codingPath: [CodingKey] {
return []
}

var userInfo: [CodingUserInfoKey : Any] {
return [:]
}
var data: MySQLData?
init() {
struct NonScalarValueSentinel: Error {}

}

func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
return .init(_KeyedValueEncoder(self))
}

func unkeyedContainer() -> UnkeyedEncodingContainer {
_UnkeyedEncoder(self)
}
var userInfo: [CodingUserInfoKey : Any] { [:] }; var codingPath: [CodingKey] { [] }
var parent: MySQLDataEncoder, value: MySQLData?

init(parent: MySQLDataEncoder) { self.parent = parent }
func container<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> { .init(_FailingKeyedContainer()) }
func unkeyedContainer() -> UnkeyedEncodingContainer { _TaintedEncoder() }
func singleValueContainer() -> SingleValueEncodingContainer {
_SingleValueEncoder(self)
}
}

struct DoJSON: Error {}

private struct _UnkeyedEncoder: UnkeyedEncodingContainer {
var codingPath: [CodingKey] {
self.encoder.codingPath
}
var count: Int {
0
}

let encoder: _Encoder
init(_ encoder: _Encoder) {
self.encoder = encoder
}


mutating func encodeNil() throws { }

mutating func encode<T>(_ value: T) throws
where T : Encodable
{ }

mutating func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type
) -> KeyedEncodingContainer<NestedKey>
where NestedKey : CodingKey
{
self.encoder.container(keyedBy: NestedKey.self)
}

mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
self.encoder.unkeyedContainer()
}

mutating func superEncoder() -> Encoder {
self.encoder
precondition(self.value == nil, "Requested multiple containers from the same encoder.")
return _SingleValueContainer(encoder: self)
}
}

private struct _KeyedValueEncoder<Key>: KeyedEncodingContainerProtocol where Key: CodingKey {
var codingPath: [CodingKey] {
return self.encoder.codingPath
}

let encoder: _Encoder
init(_ encoder: _Encoder) {
self.encoder = encoder
}

mutating func encodeNil(forKey key: Key) throws { }

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

mutating func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
self.encoder.container(keyedBy: NestedKey.self)
struct _SingleValueContainer: SingleValueEncodingContainer {
let encoder: _Encoder; var codingPath: [CodingKey] { self.encoder.codingPath }
func encodeNil() throws { self.encoder.value = .null }
func encode<T: Encodable>(_ value: T) throws { self.encoder.value = try self.encoder.parent.encode(value) }
}

mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
self.encoder.unkeyedContainer()
/// 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 NonScalarValueSentinel() }
func encode<T: Encodable>(_: T) throws { throw NonScalarValueSentinel() }
}
struct _FailingKeyedContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
var codingPath: [CodingKey] { [] }
func encodeNil(forKey: K) throws { throw NonScalarValueSentinel() }
func encode<T: Encodable>(_: T, forKey: K) throws { throw NonScalarValueSentinel() }
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() }
}

mutating func superEncoder() -> Encoder {
self.encoder
}

mutating func superEncoder(forKey key: Key) -> Encoder {
self.encoder
}
}


private struct _SingleValueEncoder: SingleValueEncodingContainer {
var codingPath: [CodingKey] {
return self.encoder.codingPath
}

let encoder: _Encoder
init(_ encoder: _Encoder) {
self.encoder = encoder
}

mutating func encodeNil() throws {
self.encoder.data = MySQLData.null
}

mutating func encode<T>(_ value: T) throws where T : Encodable {
if let convertible = value as? MySQLDataConvertible {
guard let data = convertible.mysqlData else {
throw EncodingError.invalidValue(convertible, EncodingError.Context(
codingPath: self.codingPath,
debugDescription: "Could not encode \(T.self) to MySQL data: \(value)"
))
}
self.encoder.data = data
} else {
try value.encode(to: self.encoder)
}
}
}
}

struct _Wrapper: Encodable {
let encodable: Encodable
init(_ encodable: Encodable) {
self.encodable = encodable
}
func encode(to encoder: Encoder) throws {
try self.encodable.encode(to: encoder)
}
}
53 changes: 53 additions & 0 deletions Tests/MySQLKitTests/MySQLKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,59 @@ class MySQLKitTests: XCTestCase {
XCTAssertEqual(rows, [foo])
}

/// 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 MySQLDataEncoder().encode(instance)
let encoded2 = try MySQLDataEncoder().encode([instance, instance])

XCTAssertEqual(encoded1.type, .string)
XCTAssertEqual(encoded2.type, .string)

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

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

var sql: SQLDatabase {
self.mysql.sql()
}
Expand Down