-
-
Notifications
You must be signed in to change notification settings - Fork 731
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
Codable records: JSON encoding and decoding of codable record properties #397
Changes from 4 commits
feeedcb
74aae3d
7def60c
2826472
6262d10
7f16e35
8c06e7b
a08974f
d81475a
76f5adf
c52b59f
4b7f17d
05fda18
2750980
aa8bc0f
942b32e
0cdc7d8
5f41c57
1c66ad4
820673b
358225b
b2234b4
2d6140a
de4f57c
0b77d0d
e8dacb3
b47f586
eba21d3
4d343e8
d00421a
659db4f
2eb0955
9bb4ff8
02db2a8
dfb9421
2b8c838
44b998c
7ea15dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
import Foundation | ||
|
||
private struct PersistableRecordKeyedEncodingContainer<Key: CodingKey> : KeyedEncodingContainerProtocol { | ||
let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void | ||
|
||
|
@@ -40,7 +42,23 @@ private struct PersistableRecordKeyedEncodingContainer<Key: CodingKey> : KeyedEn | |
// This allows us to encode Date as String, for example. | ||
encode((value as! DatabaseValueConvertible), key.stringValue) | ||
} else { | ||
try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) | ||
do { | ||
try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) | ||
} catch { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we carefully check for the error type before we switch to JSON decoding, but not when we switch to JSON encoding? I'd like some consistency, and the same level of care to be displayed in both places. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a oversight, fixed now that you pointed it out. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean in 6262d10? It still needs a little extra effort: just like we did for decoding, we have to check for a particular encoding error before switching to JSON. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in d81475a |
||
// If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON | ||
do { | ||
let encoder = JSONEncoder() | ||
if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { | ||
encoder.outputFormatting = .sortedKeys | ||
} | ||
let json = try encoder.encode(value) | ||
//the Data from the encoder is guaranteed to convert to String | ||
let modelAsString = String(data: json, encoding: .utf8)! | ||
return encode(modelAsString, key.stringValue) | ||
} catch { | ||
throw(error) | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -169,7 +187,8 @@ private struct PersistableRecordEncoder : Encoder { | |
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> { | ||
// Asked for a keyed type: top level required | ||
guard codingPath.isEmpty else { | ||
fatalError("unkeyed encoding is not supported") | ||
let error = EncodingError.invalidValue(encode, EncodingError.Context(codingPath: codingPath, debugDescription: "unkeyed encoding is not supported")) | ||
return KeyedEncodingContainer(ThrowingKeyedContainer(error: error)) | ||
} | ||
return KeyedEncodingContainer(PersistableRecordKeyedEncodingContainer<Key>(encode: encode)) | ||
} | ||
|
@@ -180,7 +199,8 @@ private struct PersistableRecordEncoder : Encoder { | |
/// - precondition: May not be called after a prior `self.container(keyedBy:)` call. | ||
/// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. | ||
func unkeyedContainer() -> UnkeyedEncodingContainer { | ||
fatalError("unkeyed encoding is not supported") | ||
let error = EncodingError.invalidValue(encode, EncodingError.Context(codingPath: [], debugDescription: "unkeyed encoding is not supported")) | ||
return ThrowingUnkeyedContainer(error: error) | ||
} | ||
|
||
/// Returns an encoding container appropriate for holding a single primitive value. | ||
|
@@ -194,6 +214,92 @@ private struct PersistableRecordEncoder : Encoder { | |
} | ||
} | ||
|
||
class ThrowingKeyedContainer<KeyType: CodingKey>: KeyedEncodingContainerProtocol { | ||
let errorMessage: Error | ||
var codingPath: [CodingKey] = [] | ||
|
||
init(error: Error) { | ||
errorMessage = error | ||
} | ||
|
||
func encodeNil(forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Bool, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Int, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Int8, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Int16, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Int32, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Int64, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: UInt, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: UInt8, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: UInt16, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: UInt32, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: UInt64, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Float, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: Double, forKey key: KeyType) throws { throw errorMessage } | ||
func encode(_ value: String, forKey key: KeyType) throws { throw errorMessage } | ||
func encode<T>(_ value: T, forKey key: KeyType) throws where T : Encodable { throw errorMessage } | ||
|
||
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: KeyType) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey { | ||
fatalError("Not implemented") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should return another throwing container here. Imagine a codable type that tries to access a nested container before it stores its properties and triggers one of our throwing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { | ||
fatalError("Not implemented") | ||
} | ||
func superEncoder() -> Encoder { | ||
fatalError("Not implemented") | ||
} | ||
|
||
func superEncoder(forKey key: KeyType) -> Encoder { | ||
fatalError("Not implemented") | ||
} | ||
} | ||
|
||
class ThrowingUnkeyedContainer: UnkeyedEncodingContainer { | ||
let errorMessage: Error | ||
var codingPath: [CodingKey] = [] | ||
var count: Int = 0 | ||
|
||
init(error: Error) { | ||
errorMessage = error | ||
} | ||
|
||
func encode(_ value: Int) throws { throw errorMessage } | ||
func encode(_ value: Int8) throws { throw errorMessage } | ||
func encode(_ value: Int16) throws { throw errorMessage } | ||
func encode(_ value: Int32) throws { throw errorMessage } | ||
func encode(_ value: Int64) throws { throw errorMessage } | ||
func encode(_ value: UInt) throws { throw errorMessage } | ||
func encode(_ value: UInt8) throws { throw errorMessage } | ||
func encode(_ value: UInt16) throws { throw errorMessage } | ||
func encode(_ value: UInt32) throws { throw errorMessage } | ||
func encode(_ value: UInt64) throws { throw errorMessage } | ||
func encode(_ value: Float) throws { throw errorMessage } | ||
func encode(_ value: Double) throws { throw errorMessage } | ||
func encode(_ value: String) throws { throw errorMessage } | ||
func encode<T>(_ value: T) throws where T : Encodable { throw errorMessage } | ||
func encode(_ value: Bool) throws { throw errorMessage } | ||
func encodeNil() throws { throw errorMessage } | ||
|
||
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey { | ||
fatalError("Not implemented") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above: there should be no fatalError in this type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. commit c52b59f also returns another throwing container here |
||
|
||
} | ||
|
||
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { | ||
fatalError("Not implemented") | ||
|
||
} | ||
|
||
func superEncoder() -> Encoder { | ||
fatalError("Not implemented") | ||
|
||
} | ||
} | ||
|
||
enum JsonStringError: Error { | ||
case covertStringError(String) | ||
} | ||
|
||
extension MutablePersistableRecord where Self: Encodable { | ||
public func encode(to container: inout PersistenceContainer) { | ||
// The inout container parameter won't enter an escaping closure since | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the reason for
if let _ = value as? Codable
. The commentSupport for nested ( Codable )
doesn't help me understand.we are decoding twice here, which looks like a big waste of time.
why do we check for
Codable
, since we are only decoding? Isn'tDecodable
enough?how on Earth this condition could be false, since we are decoding a
Decodable
type?All this code is pretty obscure to me. What do I miss?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure this is the best but the way the current code is basically considering the Encodable compliance a such 4 tests hit this code the following way :
if let _ = value as? Codable {
//testOptionalNestedStruct
} else {
//testDecodableRawRepresentableProperty
//testNonTrivialSingleValueDecodableProperty
//testTrivialSingleValueDecodableProperty
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the details. Please provide an answer to other questions as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want you to understand, @valexa, that the
if let _ = value as? Codable
test has to go.That's because it would have GRDB behave differently if I start decoding this type:
And then I decide that MyStruct should be Encodable as well:
In the first version of MyStruct,
if let _ = value as? Codable
doesn't pass. In the second version,if let _ = value as? Codable
passes. Decoding will change. Just because I added a conformance to MyStruct, totally unrelated to decoding.This is an important bug that has to be fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've downloaded your code, and run the tests. I see the problem you are facing. Please hold on until I find the proper way out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right, the problem is that we use a RowDecoder (dedicated to decoding a row) when we decode a column value. That decoder happily builds keyed and unkeyed containers. But we don't want to build keyed and unkeyed containers when we decode a column value! It is quite the contrary: we want a decoding error, so that we can switch to JSON. Conclusion: RowDecoder is not the decoder we need here. We need another decoder instead. I'll take care of fixing this one. Please make sure I can push to your branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done