-
-
Notifications
You must be signed in to change notification settings - Fork 727
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 1 commit
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 RowKeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtocol { | ||
let decoder: RowDecoder | ||
|
||
|
@@ -79,7 +81,17 @@ private struct RowKeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainer | |
} else if dbValue.isNull { | ||
return nil | ||
} else { | ||
return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) | ||
do { | ||
let natural = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) | ||
return natural | ||
} 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. I wonder if we could catch precisely the error emitted when the decoded type asks for a keyed container, and doesn't find any. This will prevent us from switching to JSON decoding for types that throw an error when they fail decoding from the SingleValueDecodingContainer. 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. well its the "unkeyed decoding is not supported" error so yes we can add a check for that 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. +1 |
||
if let data = row.dataNoCopy(named: key.stringValue), let dataString = String(data: data, encoding: .utf8), dataString.hasPrefix("[{") || dataString.hasPrefix("{"), dataString.hasSuffix("}]") || dataString.hasSuffix("}") { | ||
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. Checking for the format is useless: if data contains something that is not JSON, JSONDecoder will throw anyway. And by not checking the format, we'll also avoid turning Data into String. This is the whole point of SQLite weak typing (store string, load data) 😄 |
||
// If data looks like JSON data then decode it into model (nested model as JSON) | ||
return try JSONDecoder().decode(type.self, from: data) | ||
} else { | ||
fatalError("\(error)") | ||
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.
|
||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -116,7 +128,17 @@ private struct RowKeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainer | |
// This allows decoding Date from String, or DatabaseValue from NULL. | ||
return type.fromDatabaseValue(dbValue) as! T | ||
} else { | ||
return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) | ||
do { | ||
let natural = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) | ||
return natural | ||
} catch { | ||
if let data = row.dataNoCopy(named: key.stringValue), let dataString = String(data: data, encoding: .utf8), dataString.hasPrefix("[{") || dataString.hasPrefix("{"), dataString.hasSuffix("}]") || dataString.hasSuffix("}") { | ||
// If data looks like JSON data then decode it into model (nested model as JSON) | ||
return try JSONDecoder().decode(type.self, from: data) | ||
} else { | ||
fatalError("\(error)") | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
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,21 @@ 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 | ||
let encodeError = error | ||
do { | ||
let json = try JSONEncoder().encode(value) | ||
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. Please set encoder's outputFormatting to 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'd also like that we think about which dateEncodingStrategy we should use. I don't quite like that we use the default format, which is not portable at all to other platforms (intervalSinceReferenceDate is specific to Foundation as far as I know). I'm thinking about people that share databases with Android: I don't want the JSON produced by GRDB to be painful for them to decode. 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. We were thinking of the same thing but .sortedKeys is iOS 11 only 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. Then we'll sort keys on iOS11 only. 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. And make this point clear in the documentation (that record comparison may give odd results for records stored before iOS 11) 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. Precisely speaking: iOS 11.0+, macOS 10.13+, watchOS 4.0+ |
||
guard let modelAsString = String(data: json, encoding: .utf8) else { | ||
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 |
||
throw JsonStringError.covertStringError("Error, could not make string out of JSON data") | ||
} | ||
return encode(modelAsString, key.stringValue) | ||
} catch { | ||
fatalError("Encode error: \(encodeError), tried to encode to Json, got error: \(error)") | ||
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. We should throw, and not fatalError here. I understand that we hesitate between rethrowing |
||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -169,7 +185,7 @@ 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") | ||
return KeyedEncodingContainer(ThrowingKeyedContainer(error: EncodingError.invalidValue(codingPath.isEmpty, EncodingError.Context(codingPath: codingPath, debugDescription: "unkeyed encoding is not supported")))) | ||
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. We should immediately throw an error, instead of creating another level through ThrowingKeyedContainer. This error will be caught during "natural" encoding, and trigger JSON encoding. Ideally, this error should be a standard EncodingError. And it will allow us to apply my comment on https://github.com/groue/GRDB.swift/pull/397/files#diff-e9cae7906e97e4aadddcbc90115862c3R87. Eventually we discover that we don't need ThrowingKeyedContainer at all. 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. If we change func container(keyedBy type: Key.Type) -> KeyedEncodingContainer to throw we lose conformity to the Encoder protocol 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. Ha, I understand: |
||
} | ||
return KeyedEncodingContainer(PersistableRecordKeyedEncodingContainer<Key>(encode: encode)) | ||
} | ||
|
@@ -180,7 +196,7 @@ 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") | ||
return ThrowingUnkeyedContainer(error: EncodingError.invalidValue(encode, EncodingError.Context(codingPath: [], debugDescription: "unkeyed encoding is not supported"))) | ||
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 as |
||
} | ||
|
||
/// Returns an encoding container appropriate for holding a single primitive value. | ||
|
@@ -194,6 +210,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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -301,3 +301,197 @@ extension FetchableRecordDecodableTests { | |
XCTAssertEqual(value.uuid, uuid) | ||
} | ||
} | ||
|
||
// MARK: - Custom nested Decodable types - nested saved as JSON | ||
|
||
extension FetchableRecordDecodableTests { | ||
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 still don't see any test for detached rows. Context and guidance. 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. Hi, we are having issues to even begin imagining how to test for detached rows, are there any existing non Codable detached rows tests we could look at for guidance ? 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. 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. 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. @groue have just committed changes to add test for detached rows. Thank you for the example code and the description you gave to help explain detached rows. I have added JSON checking in Data.fromDatabaseValue to allow the reading of Data that is of JSON content. I did not need to add any additions to String.fromDatabaseValue as seems to work fine. 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.
Great
Something was not understood at all, here. See this comment for context.
I understand, you want to remain focused on your use case and your convenience. But please also understand that this is not enough. I manage this open source library not only for you. If GRDB had been built for my personal use cases only, you would have never heard of it. I repeat: If we support String to Data conversion, we have to support Data to String conversion. It is a matter of consistency. And it was explicitly requested in this comment. 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 should be done now in 76f5adf |
||
func testOptionalNestedStruct() throws { | ||
struct NestedStruct : PersistableRecord, FetchableRecord, Codable { | ||
let firstName = "Bob" | ||
let lastName = "Dylan" | ||
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'm glad a little music is entering the tests 👍 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. BTW... When I read I'm not saying there is a bug here. I'm saying the test would be clearer if NestedStruct was declared as: struct NestedStruct : PersistableRecord, FetchableRecord, Codable {
var firstName: String
var lastName: String
} and the insertion below written as: let value = StructWithNestedType(nested: NestedStruct(firstName: "Bob", lastName: "Dylan")) This comment applies to other testing structs with 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. Another point: NestedStruct does not need to adopt PersistableRecord and FetchableRecord. Same for all other nested structs. |
||
} | ||
|
||
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { | ||
static let databaseTableName = "t1" | ||
let nested: NestedStruct? | ||
} | ||
|
||
let dbQueue = try makeDatabaseQueue() | ||
try dbQueue.inDatabase { db in | ||
try db.create(table: "t1") { t in | ||
t.column("nested", .text) | ||
} | ||
|
||
let value = StructWithNestedType(nested: NestedStruct()) | ||
try value.insert(db) | ||
|
||
let parentModel = try StructWithNestedType.fetchAll(db) | ||
|
||
guard let nestedModel = parentModel.first?.nested else { | ||
XCTFail() | ||
return | ||
} | ||
|
||
// Check the nested model contains the expected values of first and last name | ||
XCTAssertEqual(nestedModel.firstName, "Bob") | ||
XCTAssertEqual(nestedModel.lastName, "Dylan") | ||
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. That's a good test. Checking for detached rows can be done with something like: let row: Row = ["nested": """
{ "firstName": "Bob", "lastName": "Dylan" }
"""]
let model = StructWithNestedType(row: row)
XCTAssertEqual(model.nestedModel.firstName, "Bob")
XCTAssertEqual(model.nestedModel.lastName, "Dylan") 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. Thats interesting, where if at all would we test for detached rows ? It is still not clear to us what the implications of a "detached" row are. 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. "Raw rows" and "detached rows" are two different types of rows, which don't access values in the same way. Both share the same Swift type Since they don't access values in the same way, both code paths have to be tested. This is especially true in our case, because we store JSON strings, but load JSON data. We rely on automatic conversion from string to data. This conversion is done by SQLite when a raw row is involved. It works, as you can see. But this conversion is not done by SQLite for detached rows: it has to be done by GRDB itself. I know very well that this is not done today: the test above will fail if you run it (try, you will see - if the test pass, then there is something wrong in our code). To make the test pass with detached rows, we need GRDB to provide the same automatic conversion as SQLite from string to data (and from data to string because I don't like half-baked features). This has to be done in the conversion methods I gave above: |
||
} | ||
} | ||
|
||
func testOptionalNestedStructNil() throws { | ||
struct NestedStruct : PersistableRecord, FetchableRecord, Codable { | ||
let firstName = "Bob" | ||
let lastName = "Dylan" | ||
} | ||
|
||
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { | ||
static let databaseTableName = "t1" | ||
let nested: NestedStruct? | ||
} | ||
|
||
let dbQueue = try makeDatabaseQueue() | ||
try dbQueue.inDatabase { db in | ||
try db.create(table: "t1") { t in | ||
t.column("nested", .text) | ||
} | ||
|
||
let value = StructWithNestedType(nested: nil) | ||
try value.insert(db) | ||
|
||
let parentModel = try StructWithNestedType.fetchAll(db) | ||
|
||
XCTAssertNil(parentModel.first?.nested) | ||
} | ||
} | ||
|
||
func testOptionalNestedArrayStruct() throws { | ||
struct NestedStruct : PersistableRecord, FetchableRecord, Codable { | ||
let firstName = "Bob" | ||
let lastName = "Dylan" | ||
} | ||
|
||
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { | ||
static let databaseTableName = "t1" | ||
let nested: [NestedStruct]? | ||
} | ||
|
||
let dbQueue = try makeDatabaseQueue() | ||
try dbQueue.inDatabase { db in | ||
try db.create(table: "t1") { t in | ||
t.column("nested", .text) | ||
} | ||
|
||
let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) | ||
try value.insert(db) | ||
|
||
let parentModel = try StructWithNestedType.fetchAll(db) | ||
|
||
guard let arrayOfNestedModel = parentModel.first?.nested, let firstNestedModelInArray = arrayOfNestedModel.first else { | ||
XCTFail() | ||
return | ||
} | ||
|
||
// Check there are two models in array | ||
XCTAssertTrue(arrayOfNestedModel.count == 2) | ||
|
||
// Check the nested model contains the expected values of first and last name | ||
XCTAssertEqual(firstNestedModelInArray.firstName, "Bob") | ||
XCTAssertEqual(firstNestedModelInArray.lastName, "Dylan") | ||
} | ||
} | ||
|
||
func testOptionalNestedArrayStructNil() throws { | ||
struct NestedStruct : PersistableRecord, FetchableRecord, Codable { | ||
let firstName = "Bob" | ||
let lastName = "Dylan" | ||
} | ||
|
||
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { | ||
static let databaseTableName = "t1" | ||
let nested: [NestedStruct]? | ||
} | ||
|
||
let dbQueue = try makeDatabaseQueue() | ||
try dbQueue.inDatabase { db in | ||
try db.create(table: "t1") { t in | ||
t.column("nested", .text) | ||
} | ||
|
||
let value = StructWithNestedType(nested: nil) | ||
try value.insert(db) | ||
|
||
let parentModel = try StructWithNestedType.fetchAll(db) | ||
|
||
XCTAssertNil(parentModel.first?.nested) | ||
} | ||
} | ||
|
||
func testNonOptionalNestedStruct() throws { | ||
struct NestedStruct : PersistableRecord, FetchableRecord, Codable { | ||
let firstName = "Bob" | ||
let lastName = "Dylan" | ||
} | ||
|
||
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { | ||
static let databaseTableName = "t1" | ||
let nested: NestedStruct | ||
} | ||
|
||
let dbQueue = try makeDatabaseQueue() | ||
try dbQueue.inDatabase { db in | ||
try db.create(table: "t1") { t in | ||
t.column("nested", .text) | ||
} | ||
|
||
let value = StructWithNestedType(nested: NestedStruct()) | ||
try value.insert(db) | ||
|
||
let parentModel = try StructWithNestedType.fetchAll(db) | ||
|
||
guard let nestedModel = parentModel.first?.nested else { | ||
XCTFail() | ||
return | ||
} | ||
|
||
// Check the nested model contains the expected values of first and last name | ||
XCTAssertEqual(nestedModel.firstName, "Bob") | ||
XCTAssertEqual(nestedModel.lastName, "Dylan") | ||
} | ||
} | ||
|
||
func testNonOptionalNestedArrayStruct() throws { | ||
struct NestedStruct : PersistableRecord, FetchableRecord, Codable { | ||
let firstName = "Bob" | ||
let lastName = "Dylan" | ||
} | ||
|
||
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { | ||
static let databaseTableName = "t1" | ||
let nested: [NestedStruct] | ||
} | ||
|
||
let dbQueue = try makeDatabaseQueue() | ||
try dbQueue.inDatabase { db in | ||
try db.create(table: "t1") { t in | ||
t.column("nested", .text) | ||
} | ||
|
||
let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) | ||
try value.insert(db) | ||
|
||
let parentModel = try StructWithNestedType.fetchAll(db) | ||
|
||
guard let arrayOfNestedModel = parentModel.first?.nested, let firstNestedModelInArray = arrayOfNestedModel.first else { | ||
XCTFail() | ||
return | ||
} | ||
|
||
// Check there are two models in array | ||
XCTAssertTrue(arrayOfNestedModel.count == 2) | ||
|
||
// Check the nested model contains the expected values of first and last name | ||
XCTAssertEqual(firstNestedModelInArray.firstName, "Bob") | ||
XCTAssertEqual(firstNestedModelInArray.lastName, "Dylan") | ||
} | ||
} | ||
} |
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 think
return try T(from...)
is enough. That "natural" word was useful for communication between us, but has no real purpose in the code.A comment like the following will help making the code more clear:
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