Skip to content

Commit f509c34

Browse files
committed
Add protocol for fetching and container for upserting managed objects
1 parent 9e28aac commit f509c34

File tree

5 files changed

+245
-19
lines changed

5 files changed

+245
-19
lines changed

CoreDataCodable.xcodeproj/project.pbxproj

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88

99
/* Begin PBXBuildFile section */
1010
45D98D092240FEBC0094923D /* CoreDataCodable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45D98CFF2240FEBC0094923D /* CoreDataCodable.framework */; };
11-
45D98D0E2240FEBC0094923D /* DecodableManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D98D0D2240FEBC0094923D /* DecodableManagedObjectTests.swift */; };
12-
45D98D102240FEBC0094923D /* CoreDataCodable.h in Headers */ = {isa = PBXBuildFile; fileRef = 45D98D022240FEBC0094923D /* CoreDataCodable.h */; settings = {ATTRIBUTES = (Public, ); }; };
13-
45D98D1A2240FF0F0094923D /* DecodableManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D98D192240FF0F0094923D /* DecodableManagedObject.swift */; };
14-
45D98D1C2240FF4F0094923D /* CodingUserInfoKey+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D98D1B2240FF4F0094923D /* CodingUserInfoKey+ManagedObjectContext.swift */; };
1511
45D98D202240FF8B0094923D /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 45D98D1E2240FF8B0094923D /* Model.xcdatamodeld */; };
1612
45D98D222240FF970094923D /* PersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D98D212240FF970094923D /* PersistentContainer.swift */; };
17-
45D98D24224103820094923D /* CodingUserInfoKey+ManagedObjectContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D98D23224103820094923D /* CodingUserInfoKey+ManagedObjectContextTests.swift */; };
13+
45FE1BE72245175200CADDF2 /* CoreDataCodable.h in Headers */ = {isa = PBXBuildFile; fileRef = 45D98D022240FEBC0094923D /* CoreDataCodable.h */; settings = {ATTRIBUTES = (Public, ); }; };
14+
45FE1BEE224517A800CADDF2 /* DecodableManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BEA224517A700CADDF2 /* DecodableManagedObject.swift */; };
15+
45FE1BEF224517A800CADDF2 /* FetchableManageObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BEB224517A700CADDF2 /* FetchableManageObject.swift */; };
16+
45FE1BF0224517A800CADDF2 /* CodingUserInfoKey+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BEC224517A700CADDF2 /* CodingUserInfoKey+ManagedObjectContext.swift */; };
17+
45FE1BF1224517A800CADDF2 /* ManagedObjectUpsert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BED224517A700CADDF2 /* ManagedObjectUpsert.swift */; };
18+
45FE1BF6224517B800CADDF2 /* CodingUserInfoKey+ManagedObjectContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BF2224517B800CADDF2 /* CodingUserInfoKey+ManagedObjectContextTests.swift */; };
19+
45FE1BF7224517B800CADDF2 /* DecodableManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BF3224517B800CADDF2 /* DecodableManagedObjectTests.swift */; };
20+
45FE1BF8224517B800CADDF2 /* ManagedObjectUpsertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BF4224517B800CADDF2 /* ManagedObjectUpsertTests.swift */; };
21+
45FE1BF9224517B800CADDF2 /* FetchableManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE1BF5224517B800CADDF2 /* FetchableManagedObjectTests.swift */; };
1822
/* End PBXBuildFile section */
1923

2024
/* Begin PBXContainerItemProxy section */
@@ -32,13 +36,17 @@
3236
45D98D022240FEBC0094923D /* CoreDataCodable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataCodable.h; sourceTree = "<group>"; };
3337
45D98D032240FEBC0094923D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3438
45D98D082240FEBC0094923D /* CoreDataCodableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreDataCodableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
35-
45D98D0D2240FEBC0094923D /* DecodableManagedObjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableManagedObjectTests.swift; sourceTree = "<group>"; };
3639
45D98D0F2240FEBC0094923D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
37-
45D98D192240FF0F0094923D /* DecodableManagedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableManagedObject.swift; sourceTree = "<group>"; };
38-
45D98D1B2240FF4F0094923D /* CodingUserInfoKey+ManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingUserInfoKey+ManagedObjectContext.swift"; sourceTree = "<group>"; };
3940
45D98D1F2240FF8B0094923D /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
4041
45D98D212240FF970094923D /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = "<group>"; };
41-
45D98D23224103820094923D /* CodingUserInfoKey+ManagedObjectContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingUserInfoKey+ManagedObjectContextTests.swift"; sourceTree = "<group>"; };
42+
45FE1BEA224517A700CADDF2 /* DecodableManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecodableManagedObject.swift; sourceTree = "<group>"; };
43+
45FE1BEB224517A700CADDF2 /* FetchableManageObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableManageObject.swift; sourceTree = "<group>"; };
44+
45FE1BEC224517A700CADDF2 /* CodingUserInfoKey+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CodingUserInfoKey+ManagedObjectContext.swift"; sourceTree = "<group>"; };
45+
45FE1BED224517A700CADDF2 /* ManagedObjectUpsert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObjectUpsert.swift; sourceTree = "<group>"; };
46+
45FE1BF2224517B800CADDF2 /* CodingUserInfoKey+ManagedObjectContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CodingUserInfoKey+ManagedObjectContextTests.swift"; sourceTree = "<group>"; };
47+
45FE1BF3224517B800CADDF2 /* DecodableManagedObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecodableManagedObjectTests.swift; sourceTree = "<group>"; };
48+
45FE1BF4224517B800CADDF2 /* ManagedObjectUpsertTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObjectUpsertTests.swift; sourceTree = "<group>"; };
49+
45FE1BF5224517B800CADDF2 /* FetchableManagedObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableManagedObjectTests.swift; sourceTree = "<group>"; };
4250
/* End PBXFileReference section */
4351

4452
/* Begin PBXFrameworksBuildPhase section */
@@ -81,21 +89,25 @@
8189
45D98D012240FEBC0094923D /* CoreDataCodable */ = {
8290
isa = PBXGroup;
8391
children = (
84-
45D98D1B2240FF4F0094923D /* CodingUserInfoKey+ManagedObjectContext.swift */,
92+
45FE1BEC224517A700CADDF2 /* CodingUserInfoKey+ManagedObjectContext.swift */,
8593
45D98D022240FEBC0094923D /* CoreDataCodable.h */,
86-
45D98D192240FF0F0094923D /* DecodableManagedObject.swift */,
94+
45FE1BEA224517A700CADDF2 /* DecodableManagedObject.swift */,
95+
45FE1BEB224517A700CADDF2 /* FetchableManageObject.swift */,
8796
45D98D032240FEBC0094923D /* Info.plist */,
97+
45FE1BED224517A700CADDF2 /* ManagedObjectUpsert.swift */,
8898
);
8999
path = CoreDataCodable;
90100
sourceTree = "<group>";
91101
};
92102
45D98D0C2240FEBC0094923D /* CoreDataCodableTests */ = {
93103
isa = PBXGroup;
94104
children = (
95-
45D98D23224103820094923D /* CodingUserInfoKey+ManagedObjectContextTests.swift */,
105+
45FE1BF2224517B800CADDF2 /* CodingUserInfoKey+ManagedObjectContextTests.swift */,
96106
45D98D1D2240FF770094923D /* Core data */,
97-
45D98D0D2240FEBC0094923D /* DecodableManagedObjectTests.swift */,
107+
45FE1BF3224517B800CADDF2 /* DecodableManagedObjectTests.swift */,
108+
45FE1BF5224517B800CADDF2 /* FetchableManagedObjectTests.swift */,
98109
45D98D0F2240FEBC0094923D /* Info.plist */,
110+
45FE1BF4224517B800CADDF2 /* ManagedObjectUpsertTests.swift */,
99111
);
100112
path = CoreDataCodableTests;
101113
sourceTree = "<group>";
@@ -116,7 +128,7 @@
116128
isa = PBXHeadersBuildPhase;
117129
buildActionMask = 2147483647;
118130
files = (
119-
45D98D102240FEBC0094923D /* CoreDataCodable.h in Headers */,
131+
45FE1BE72245175200CADDF2 /* CoreDataCodable.h in Headers */,
120132
);
121133
runOnlyForDeploymentPostprocessing = 0;
122134
};
@@ -218,20 +230,23 @@
218230
isa = PBXSourcesBuildPhase;
219231
buildActionMask = 2147483647;
220232
files = (
221-
45D98D1A2240FF0F0094923D /* DecodableManagedObject.swift in Sources */,
222-
45D98D1A2240FF0F0094923D /* DecodableManagedObject.swift in Sources */,
223-
45D98D1C2240FF4F0094923D /* CodingUserInfoKey+ManagedObjectContext.swift in Sources */,
233+
45FE1BEF224517A800CADDF2 /* FetchableManageObject.swift in Sources */,
234+
45FE1BEE224517A800CADDF2 /* DecodableManagedObject.swift in Sources */,
235+
45FE1BF0224517A800CADDF2 /* CodingUserInfoKey+ManagedObjectContext.swift in Sources */,
236+
45FE1BF1224517A800CADDF2 /* ManagedObjectUpsert.swift in Sources */,
224237
);
225238
runOnlyForDeploymentPostprocessing = 0;
226239
};
227240
45D98D042240FEBC0094923D /* Sources */ = {
228241
isa = PBXSourcesBuildPhase;
229242
buildActionMask = 2147483647;
230243
files = (
231-
45D98D0E2240FEBC0094923D /* DecodableManagedObjectTests.swift in Sources */,
232244
45D98D202240FF8B0094923D /* Model.xcdatamodeld in Sources */,
233245
45D98D222240FF970094923D /* PersistentContainer.swift in Sources */,
234-
45D98D24224103820094923D /* CodingUserInfoKey+ManagedObjectContextTests.swift in Sources */,
246+
45FE1BF8224517B800CADDF2 /* ManagedObjectUpsertTests.swift in Sources */,
247+
45FE1BF7224517B800CADDF2 /* DecodableManagedObjectTests.swift in Sources */,
248+
45FE1BF6224517B800CADDF2 /* CodingUserInfoKey+ManagedObjectContextTests.swift in Sources */,
249+
45FE1BF9224517B800CADDF2 /* FetchableManagedObjectTests.swift in Sources */,
235250
);
236251
runOnlyForDeploymentPostprocessing = 0;
237252
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// FetchableManageObject.swift
3+
// CoreDataCodable
4+
//
5+
// Created by Peter Ringset on 19/03/2019.
6+
// Copyright © 2019 Ringset. All rights reserved.
7+
//
8+
9+
import CoreData
10+
import Foundation
11+
12+
public protocol FetchableManagedObject {
13+
14+
associatedtype FetchableCodingKeys: CodingKey
15+
associatedtype Identifier: Decodable & CVarArg
16+
static var identifierKey: FetchableCodingKeys { get }
17+
18+
}
19+
20+
extension FetchableManagedObject where Self: NSManagedObject {
21+
22+
static func fetch(from decoder: Decoder) throws -> Self? {
23+
let context = try decoder.managedObjectContext()
24+
let container = try decoder.container(keyedBy: FetchableCodingKeys.self)
25+
let identifier = try container.decode(Identifier.self, forKey: identifierKey)
26+
let request = NSFetchRequest<Self>(entityName: String(describing: Self.self))
27+
request.predicate = NSPredicate(format: "\(identifierKey.stringValue) = %@", identifier)
28+
return try context.fetch(request).first
29+
}
30+
31+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// ManagedObjectUpsert.swift
3+
// CoreDataCodable
4+
//
5+
// Created by Peter Ringset on 19/03/2019.
6+
// Copyright © 2019 Ringset. All rights reserved.
7+
//
8+
9+
import CoreData
10+
import Foundation
11+
12+
public struct ManagedObjectUpsert<Object: FetchableManagedObject & DecodableManagedObject>: Decodable where Object: NSManagedObject {
13+
14+
public let object: Object
15+
16+
public init(from decoder: Decoder) throws {
17+
if let existing = try Object.fetch(from: decoder) {
18+
try existing.setValues(from: decoder)
19+
self.object = existing
20+
} else {
21+
let container = try decoder.singleValueContainer()
22+
object = try container.decode(Object.self)
23+
}
24+
}
25+
26+
}
27+
28+
public struct ManagedObjectsUpsert<Element: FetchableManagedObject & DecodableManagedObject>: Decodable where Element: NSManagedObject {
29+
30+
public let objects: [Element]
31+
32+
public init(from decoder: Decoder) throws {
33+
let container = try decoder.singleValueContainer()
34+
objects = try container.decode([ManagedObjectUpsert<Element>].self).map({ $0.object })
35+
}
36+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// FetchableManagedObjectTests.swift
3+
// CoreDataCodableTests
4+
//
5+
// Created by Peter Ringset on 19/03/2019.
6+
// Copyright © 2019 Ringset. All rights reserved.
7+
//
8+
9+
import CoreData
10+
import XCTest
11+
12+
@testable import CoreDataCodable
13+
14+
class FetchableManagedObjectTests: XCTestCase {
15+
16+
func testFetch() {
17+
let context = DataContainer().viewContext
18+
let countRequest = NSFetchRequest<Person>(entityName: "Person")
19+
20+
XCTAssertEqual(try! context.count(for: countRequest), 0)
21+
22+
let person1 = Person(context: context)
23+
person1.name = "123"
24+
person1.address = "654"
25+
person1.postalCode = 987
26+
try! context.save()
27+
28+
XCTAssertEqual(try! context.count(for: countRequest), 1)
29+
30+
let json = """
31+
{ "name": "123" }
32+
""".data(using: .utf8)!
33+
let decoder = JSONDecoder()
34+
decoder.userInfo[.managedObjectContext] = context
35+
36+
let person2 = try! decoder.decode(Wrapper.self, from: json).value
37+
XCTAssertEqual(try! context.count(for: countRequest), 1)
38+
XCTAssertEqual(person1.objectID, person2?.objectID)
39+
}
40+
41+
}
42+
43+
extension Person: FetchableManagedObject {
44+
public typealias FetchableCodingKeys = Person.CodingKeys
45+
public typealias Identifier = String
46+
public static var identifierKey: Person.CodingKeys {
47+
return FetchableCodingKeys.name
48+
}
49+
}
50+
51+
private struct Wrapper: Decodable {
52+
let value: Person?
53+
init(from decoder: Decoder) throws {
54+
value = try Person.fetch(from: decoder)
55+
}
56+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//
2+
// ManagedObjectUpsertTests.swift
3+
// CoreDataCodableTests
4+
//
5+
// Created by Peter Ringset on 19/03/2019.
6+
// Copyright © 2019 Ringset. All rights reserved.
7+
//
8+
9+
import XCTest
10+
11+
@testable import CoreDataCodable
12+
13+
class ManagedObjectUpsertTests: XCTestCase {
14+
15+
func testInitingWithBlankDatabase() {
16+
let context = DataContainer().viewContext
17+
let json = """
18+
{
19+
"name": "lalal",
20+
"address": "ieieei",
21+
"postalCode": 11232
22+
}
23+
""".data(using: .utf8)!
24+
25+
let decoder = JSONDecoder()
26+
decoder.userInfo[.managedObjectContext] = context
27+
let inserted = try! decoder.decode(ManagedObjectUpsert<Person>.self, from: json).object
28+
XCTAssertEqual(context.insertedObjects, [inserted])
29+
}
30+
31+
func testInitingWithExistingInDatabase() {
32+
let context = DataContainer().viewContext
33+
let existing = Person(context: context)
34+
existing.name = "lalal"
35+
existing.address = ""
36+
existing.postalCode = 0
37+
try! context.save()
38+
39+
let json = """
40+
{
41+
"name": "lalal",
42+
"address": "ieieei",
43+
"postalCode": 11232
44+
}
45+
""".data(using: .utf8)!
46+
47+
let decoder = JSONDecoder()
48+
decoder.userInfo[.managedObjectContext] = context
49+
let updated = try! decoder.decode(ManagedObjectUpsert<Person>.self, from: json).object
50+
XCTAssertEqual(updated, existing)
51+
XCTAssertEqual(updated.address, "ieieei")
52+
}
53+
54+
func testInitializingArray() {
55+
let context = DataContainer().viewContext
56+
let existing = Person(context: context)
57+
existing.name = "lalal"
58+
existing.address = ""
59+
existing.postalCode = 0
60+
try! context.save()
61+
62+
let json = """
63+
[
64+
{
65+
"name": "lalal",
66+
"address": "ieieei",
67+
"postalCode": 1111
68+
},
69+
{
70+
"name": "lalal2",
71+
"address": "ieieei2",
72+
"postalCode": 2222
73+
}
74+
]
75+
""".data(using: .utf8)!
76+
77+
let decoder = JSONDecoder()
78+
decoder.userInfo[.managedObjectContext] = context
79+
80+
XCTAssertNoThrow(try decoder.decode(ManagedObjectsUpsert<Person>.self, from: json))
81+
XCTAssertEqual(context.updatedObjects.count, 1)
82+
XCTAssertEqual((context.updatedObjects.first as? Person)?.name, "lalal")
83+
XCTAssertEqual(context.insertedObjects.count, 1)
84+
XCTAssertEqual((context.insertedObjects.first as? Person)?.name, "lalal2")
85+
XCTAssertNoThrow(try context.save())
86+
}
87+
88+
}

0 commit comments

Comments
 (0)