Skip to content

Commit 7d7e6e3

Browse files
authored
Revise MySQLDataEncoder implementation to better handle various Codable conformances (#313)
Revise MySQLDataEncoder implementation to better handle various Codable conformances (no more corrupted data when superEncoder() is used)
1 parent faecdf3 commit 7d7e6e3

File tree

2 files changed

+108
-137
lines changed

2 files changed

+108
-137
lines changed

Sources/MySQLKit/MySQLDataEncoder.swift

Lines changed: 55 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,26 @@ public struct MySQLDataEncoder {
1111
if let custom = value as? MySQLDataConvertible, let data = custom.mysqlData {
1212
return data
1313
} else {
14-
let encoder = _Encoder()
15-
try value.encode(to: encoder)
16-
if let data = encoder.data {
17-
return data
18-
} else {
14+
let encoder = _Encoder(parent: self)
15+
do {
16+
try value.encode(to: encoder)
17+
if let value = encoder.value {
18+
return value
19+
} else {
20+
throw _Encoder.NonScalarValueSentinel()
21+
}
22+
} catch is _Encoder.NonScalarValueSentinel {
1923
var buffer = ByteBufferAllocator().buffer(capacity: 0)
20-
try buffer.writeBytes(self.json.encode(_Wrapper(value)))
24+
#if swift(<5.7)
25+
struct _Wrapper: Encodable {
26+
let encodable: Encodable
27+
init(_ encodable: Encodable) { self.encodable = encodable }
28+
func encode(to encoder: Encoder) throws { try self.encodable.encode(to: encoder) }
29+
}
30+
try buffer.writeBytes(self.json.encode(_Wrapper(value))) // Swift <5.7 will complain that "Encodable does not conform to Encodable" without the wrapper
31+
#else
32+
try buffer.writeBytes(self.json.encode(value))
33+
#endif
2134
return MySQLData(
2235
type: .string,
2336
format: .text,
@@ -27,145 +40,50 @@ public struct MySQLDataEncoder {
2740
}
2841
}
2942
}
30-
43+
3144
private final class _Encoder: Encoder {
32-
var codingPath: [CodingKey] {
33-
return []
34-
}
35-
36-
var userInfo: [CodingUserInfoKey : Any] {
37-
return [:]
38-
}
39-
var data: MySQLData?
40-
init() {
45+
struct NonScalarValueSentinel: Error {}
4146

42-
}
43-
44-
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
45-
return .init(_KeyedValueEncoder(self))
46-
}
47-
48-
func unkeyedContainer() -> UnkeyedEncodingContainer {
49-
_UnkeyedEncoder(self)
50-
}
47+
var userInfo: [CodingUserInfoKey : Any] { [:] }; var codingPath: [CodingKey] { [] }
48+
var parent: MySQLDataEncoder, value: MySQLData?
5149

50+
init(parent: MySQLDataEncoder) { self.parent = parent }
51+
func container<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> { .init(_FailingKeyedContainer()) }
52+
func unkeyedContainer() -> UnkeyedEncodingContainer { _TaintedEncoder() }
5253
func singleValueContainer() -> SingleValueEncodingContainer {
53-
_SingleValueEncoder(self)
54-
}
55-
}
56-
57-
struct DoJSON: Error {}
58-
59-
private struct _UnkeyedEncoder: UnkeyedEncodingContainer {
60-
var codingPath: [CodingKey] {
61-
self.encoder.codingPath
62-
}
63-
var count: Int {
64-
0
65-
}
66-
67-
let encoder: _Encoder
68-
init(_ encoder: _Encoder) {
69-
self.encoder = encoder
70-
}
71-
72-
73-
mutating func encodeNil() throws { }
74-
75-
mutating func encode<T>(_ value: T) throws
76-
where T : Encodable
77-
{ }
78-
79-
mutating func nestedContainer<NestedKey>(
80-
keyedBy keyType: NestedKey.Type
81-
) -> KeyedEncodingContainer<NestedKey>
82-
where NestedKey : CodingKey
83-
{
84-
self.encoder.container(keyedBy: NestedKey.self)
85-
}
86-
87-
mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
88-
self.encoder.unkeyedContainer()
89-
}
90-
91-
mutating func superEncoder() -> Encoder {
92-
self.encoder
54+
precondition(self.value == nil, "Requested multiple containers from the same encoder.")
55+
return _SingleValueContainer(encoder: self)
9356
}
94-
}
95-
96-
private struct _KeyedValueEncoder<Key>: KeyedEncodingContainerProtocol where Key: CodingKey {
97-
var codingPath: [CodingKey] {
98-
return self.encoder.codingPath
99-
}
100-
101-
let encoder: _Encoder
102-
init(_ encoder: _Encoder) {
103-
self.encoder = encoder
104-
}
105-
106-
mutating func encodeNil(forKey key: Key) throws { }
107-
108-
mutating func encode<T>(_ value: T, forKey key: Key) throws
109-
where T : Encodable
110-
{ }
11157

112-
mutating func nestedContainer<NestedKey>(
113-
keyedBy keyType: NestedKey.Type,
114-
forKey key: Key
115-
) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
116-
self.encoder.container(keyedBy: NestedKey.self)
58+
struct _SingleValueContainer: SingleValueEncodingContainer {
59+
let encoder: _Encoder; var codingPath: [CodingKey] { self.encoder.codingPath }
60+
func encodeNil() throws { self.encoder.value = .null }
61+
func encode<T: Encodable>(_ value: T) throws { self.encoder.value = try self.encoder.parent.encode(value) }
11762
}
11863

119-
mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
120-
self.encoder.unkeyedContainer()
64+
/// This pair of types is only necessary because we can't directly throw an error from various Encoder and
65+
/// encoding container methods. We define duplicate types rather than the old implementation's use of a
66+
/// no-action keyed container because it can save a significant amount of time otherwise spent uselessly calling
67+
/// nested methods in some cases.
68+
struct _TaintedEncoder: Encoder, UnkeyedEncodingContainer, SingleValueEncodingContainer {
69+
var userInfo: [CodingUserInfoKey : Any] { [:] }; var codingPath: [CodingKey] { [] }; var count: Int { 0 }
70+
func container<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> { .init(_FailingKeyedContainer()) }
71+
func nestedContainer<K: CodingKey>(keyedBy: K.Type) -> KeyedEncodingContainer<K> { .init(_FailingKeyedContainer()) }
72+
func unkeyedContainer() -> UnkeyedEncodingContainer { self }
73+
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { self }
74+
func singleValueContainer() -> SingleValueEncodingContainer { self }
75+
func superEncoder() -> Encoder { self }
76+
func encodeNil() throws { throw NonScalarValueSentinel() }
77+
func encode<T: Encodable>(_: T) throws { throw NonScalarValueSentinel() }
78+
}
79+
struct _FailingKeyedContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
80+
var codingPath: [CodingKey] { [] }
81+
func encodeNil(forKey: K) throws { throw NonScalarValueSentinel() }
82+
func encode<T: Encodable>(_: T, forKey: K) throws { throw NonScalarValueSentinel() }
83+
func nestedContainer<NK: CodingKey>(keyedBy: NK.Type, forKey: K) -> KeyedEncodingContainer<NK> { .init(_FailingKeyedContainer<NK>()) }
84+
func nestedUnkeyedContainer(forKey: K) -> UnkeyedEncodingContainer { _TaintedEncoder() }
85+
func superEncoder() -> Encoder { _TaintedEncoder() }
86+
func superEncoder(forKey: K) -> Encoder { _TaintedEncoder() }
12187
}
122-
123-
mutating func superEncoder() -> Encoder {
124-
self.encoder
125-
}
126-
127-
mutating func superEncoder(forKey key: Key) -> Encoder {
128-
self.encoder
129-
}
130-
}
131-
132-
133-
private struct _SingleValueEncoder: SingleValueEncodingContainer {
134-
var codingPath: [CodingKey] {
135-
return self.encoder.codingPath
136-
}
137-
138-
let encoder: _Encoder
139-
init(_ encoder: _Encoder) {
140-
self.encoder = encoder
141-
}
142-
143-
mutating func encodeNil() throws {
144-
self.encoder.data = MySQLData.null
145-
}
146-
147-
mutating func encode<T>(_ value: T) throws where T : Encodable {
148-
if let convertible = value as? MySQLDataConvertible {
149-
guard let data = convertible.mysqlData else {
150-
throw EncodingError.invalidValue(convertible, EncodingError.Context(
151-
codingPath: self.codingPath,
152-
debugDescription: "Could not encode \(T.self) to MySQL data: \(value)"
153-
))
154-
}
155-
self.encoder.data = data
156-
} else {
157-
try value.encode(to: self.encoder)
158-
}
159-
}
160-
}
161-
}
162-
163-
struct _Wrapper: Encodable {
164-
let encodable: Encodable
165-
init(_ encodable: Encodable) {
166-
self.encodable = encodable
167-
}
168-
func encode(to encoder: Encoder) throws {
169-
try self.encodable.encode(to: encoder)
17088
}
17189
}

Tests/MySQLKitTests/MySQLKitTests.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,59 @@ class MySQLKitTests: XCTestCase {
4848
XCTAssertEqual(rows, [foo])
4949
}
5050

51+
/// Tests dealing with encoding of values whose `encode(to:)` implementation calls one of the `superEncoder()`
52+
/// methods (most notably the implementation of `Codable` for Fluent's `Fields`, which we can't directly test
53+
/// at this layer).
54+
func testValuesThatUseSuperEncoder() throws {
55+
struct UnusualType: Codable {
56+
var prop1: String, prop2: [Bool], prop3: [[Bool]]
57+
58+
// This is intentionally contrived - Fluent's implementation does Codable this roundabout way as a
59+
// workaround for the interaction of property wrappers with optional properties; it serves no purpose
60+
// here other than to demonstrate that the encoder supports it.
61+
private enum CodingKeys: String, CodingKey { case prop1, prop2, prop3 }
62+
init(prop1: String, prop2: [Bool], prop3: [[Bool]]) { (self.prop1, self.prop2, self.prop3) = (prop1, prop2, prop3) }
63+
init(from decoder: Decoder) throws {
64+
let container = try decoder.container(keyedBy: CodingKeys.self)
65+
self.prop1 = try .init(from: container.superDecoder(forKey: .prop1))
66+
var acontainer = try container.nestedUnkeyedContainer(forKey: .prop2), ongoing: [Bool] = []
67+
while !acontainer.isAtEnd { ongoing.append(try Bool.init(from: acontainer.superDecoder())) }
68+
self.prop2 = ongoing
69+
var bcontainer = try container.nestedUnkeyedContainer(forKey: .prop3), bongoing: [[Bool]] = []
70+
while !bcontainer.isAtEnd {
71+
var ccontainer = try bcontainer.nestedUnkeyedContainer(), congoing: [Bool] = []
72+
while !ccontainer.isAtEnd { congoing.append(try Bool.init(from: ccontainer.superDecoder())) }
73+
bongoing.append(congoing)
74+
}
75+
self.prop3 = bongoing
76+
}
77+
func encode(to encoder: Encoder) throws {
78+
var container = encoder.container(keyedBy: CodingKeys.self)
79+
try self.prop1.encode(to: container.superEncoder(forKey: .prop1))
80+
var acontainer = container.nestedUnkeyedContainer(forKey: .prop2)
81+
for val in self.prop2 { try val.encode(to: acontainer.superEncoder()) }
82+
var bcontainer = container.nestedUnkeyedContainer(forKey: .prop3)
83+
for arr in self.prop3 {
84+
var ccontainer = bcontainer.nestedUnkeyedContainer()
85+
for val in arr { try val.encode(to: ccontainer.superEncoder()) }
86+
}
87+
}
88+
}
89+
90+
let instance = UnusualType(prop1: "hello", prop2: [true, false, false, true], prop3: [[true, true], [false], [true], []])
91+
let encoded1 = try MySQLDataEncoder().encode(instance)
92+
let encoded2 = try MySQLDataEncoder().encode([instance, instance])
93+
94+
XCTAssertEqual(encoded1.type, .string)
95+
XCTAssertEqual(encoded2.type, .string)
96+
97+
let decoded1 = try MySQLDataDecoder().decode(UnusualType.self, from: encoded1)
98+
let decoded2 = try MySQLDataDecoder().decode([UnusualType].self, from: encoded2)
99+
100+
XCTAssertEqual(decoded1.prop3, instance.prop3)
101+
XCTAssertEqual(decoded2.count, 2)
102+
}
103+
51104
var sql: SQLDatabase {
52105
self.mysql.sql()
53106
}

0 commit comments

Comments
 (0)