Skip to content

Commit 08281d0

Browse files
authored
Merge pull request #396 from woocommerce/issue/19-notifications-yosemite
Notifications: Added NotificationStore and friends
2 parents 4241960 + 5edb4f4 commit 08281d0

File tree

19 files changed

+609
-44
lines changed

19 files changed

+609
-44
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
74046E21217A73D0007DD7BF /* settings-general.json in Resources */ = {isa = PBXBuildFile; fileRef = 74046E20217A73D0007DD7BF /* settings-general.json */; };
1818
7412A51121702E9700994370 /* order-stats-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = 7412A51021702E9700994370 /* order-stats-alt.json */; };
1919
741B950120EBC8A700DD6E2D /* OrderCouponLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 741B950020EBC8A700DD6E2D /* OrderCouponLine.swift */; };
20+
743057B5218B6ACD00441A76 /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743057B4218B6ACD00441A76 /* Queue.swift */; };
2021
743BF8BE21191B63008A9D87 /* site-visits.json in Resources */ = {isa = PBXBuildFile; fileRef = 743BF8BD21191B63008A9D87 /* site-visits.json */; };
2122
743FDB9C210FB36900AC737F /* order-stats-month.json in Resources */ = {isa = PBXBuildFile; fileRef = 743FDB99210FB36900AC737F /* order-stats-month.json */; };
2223
743FDB9D210FB36900AC737F /* order-stats-year.json in Resources */ = {isa = PBXBuildFile; fileRef = 743FDB9A210FB36900AC737F /* order-stats-year.json */; };
@@ -158,6 +159,7 @@
158159
74046E20217A73D0007DD7BF /* settings-general.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "settings-general.json"; sourceTree = "<group>"; };
159160
7412A51021702E9700994370 /* order-stats-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-alt.json"; sourceTree = "<group>"; };
160161
741B950020EBC8A700DD6E2D /* OrderCouponLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCouponLine.swift; sourceTree = "<group>"; };
162+
743057B4218B6ACD00441A76 /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Queue.swift; path = ../../Yosemite/Yosemite/Internal/Queue.swift; sourceTree = "<group>"; };
161163
743BF8BD21191B63008A9D87 /* site-visits.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-visits.json"; sourceTree = "<group>"; };
162164
743FDB99210FB36900AC737F /* order-stats-month.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-month.json"; sourceTree = "<group>"; };
163165
743FDB9A210FB36900AC737F /* order-stats-year.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-year.json"; sourceTree = "<group>"; };
@@ -575,6 +577,7 @@
575577
B5A0369F214C0F4C00774E2C /* Internal */ = {
576578
isa = PBXGroup;
577579
children = (
580+
743057B4218B6ACD00441A76 /* Queue.swift */,
578581
B5A036A0214C0F5400774E2C /* CocoaLumberjack.swift */,
579582
);
580583
name = Internal;
@@ -865,6 +868,7 @@
865868
B557DA0D20975DB1005962F4 /* WordPressAPIVersion.swift in Sources */,
866869
74A1D26F21189EA100931DFA /* SiteVisitStatsRemote.swift in Sources */,
867870
B557DA1D20979E7D005962F4 /* Order.swift in Sources */,
871+
743057B5218B6ACD00441A76 /* Queue.swift in Sources */,
868872
74A1D26821189A7100931DFA /* SiteVisitStats.swift in Sources */,
869873
B557DA0320975500005962F4 /* Remote.swift in Sources */,
870874
748D424C210FA34400CF7D1B /* OrderStatsMapper.swift in Sources */,

Networking/Networking/Model/Note.swift

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Foundation
55
//
66
public struct Note {
77

8+
89
/// Notification's Primary Key.
910
///
1011
public let noteId: Int64
@@ -49,22 +50,41 @@ public struct Note {
4950
///
5051
public let title: String?
5152

52-
/// Raw Subject Blocks.
53+
54+
/// Raw Subject Blocks as Data.
55+
///
56+
public let subjectAsData: Data
57+
58+
/// Subject Blocks.
5359
///
5460
public let subject: [NoteBlock]
5561

56-
/// Raw Header Blocks.
62+
63+
/// Raw Header Blocks as Data.
64+
///
65+
public let headerAsData: Data
66+
67+
/// Header Blocks.
5768
///
5869
public let header: [NoteBlock]
5970

60-
/// Raw Body Blocks.
71+
72+
/// Raw Body Blocks as Data.
73+
///
74+
public let bodyAsData: Data
75+
76+
/// Body Blocks.
6177
///
6278
public let body: [NoteBlock]
6379

64-
/// Raw Associated Metadata.
80+
81+
/// Raw Associated Metadata as Data.
6582
///
66-
public let meta: MetaContainer
83+
public let metaAsData: Data
6784

85+
/// Associated Metadata.
86+
///
87+
public let meta: MetaContainer
6888

6989

7090
/// Designed Initializer.
@@ -78,10 +98,10 @@ public struct Note {
7898
type: String,
7999
url: String?,
80100
title: String?,
81-
subject: [NoteBlock],
82-
header: [NoteBlock],
83-
body: [NoteBlock],
84-
meta: [String: AnyCodable]) {
101+
subject: Data,
102+
header: Data,
103+
body: Data,
104+
meta: Data) {
85105

86106
self.noteId = noteId
87107
self.hash = hash
@@ -94,10 +114,19 @@ public struct Note {
94114
self.kind = Kind(rawValue: type) ?? .unknown
95115
self.url = url
96116
self.title = title
97-
self.subject = subject
98-
self.header = header
99-
self.body = body
100-
self.meta = MetaContainer(payload: meta)
117+
118+
self.subjectAsData = subject
119+
self.subject = (try? JSONDecoder().decode([NoteBlock].self, from: subject)) ?? []
120+
121+
self.headerAsData = header
122+
self.header = (try? JSONDecoder().decode([NoteBlock].self, from: header)) ?? []
123+
124+
self.bodyAsData = body
125+
self.body = (try? JSONDecoder().decode([NoteBlock].self, from: body)) ?? []
126+
127+
self.metaAsData = meta
128+
let metaDict = (try? JSONDecoder().decode([String: AnyCodable].self, from: meta)) ?? [:]
129+
self.meta = MetaContainer(payload: metaDict)
101130
}
102131
}
103132

@@ -122,10 +151,17 @@ extension Note: Decodable {
122151
let url = container.failsafeDecodeIfPresent(String.self, forKey: .url)
123152
let title = container.failsafeDecodeIfPresent(String.self, forKey: .title)
124153

125-
let subject = container.failsafeDecodeIfPresent([NoteBlock].self, forKey: .subject) ?? []
126-
let header = container.failsafeDecodeIfPresent([NoteBlock].self, forKey: .header) ?? []
127-
let body = container.failsafeDecodeIfPresent([NoteBlock].self, forKey: .body) ?? []
128-
let meta = container.failsafeDecodeIfPresent([String: AnyCodable].self, forKey: .meta) ?? [:]
154+
let rawSubjectAsData = container.failsafeDecodeIfPresent([AnyCodable].self, forKey: .subject) ?? []
155+
let subjectAsData = try JSONEncoder().encode(rawSubjectAsData)
156+
157+
let rawHeaderAsData = container.failsafeDecodeIfPresent([AnyCodable].self, forKey: .header) ?? []
158+
let headerAsData = try JSONEncoder().encode(rawHeaderAsData)
159+
160+
let rawBodyAsData = container.failsafeDecodeIfPresent([AnyCodable].self, forKey: .body) ?? []
161+
let bodyAsData = try JSONEncoder().encode(rawBodyAsData)
162+
163+
let rawMetaAsData = container.failsafeDecodeIfPresent([String: AnyCodable].self, forKey: .meta) ?? [:]
164+
let metaAsData = try JSONEncoder().encode(rawMetaAsData)
129165

130166
self.init(noteId: noteID,
131167
hash: hash,
@@ -136,10 +172,10 @@ extension Note: Decodable {
136172
type: type,
137173
url: url,
138174
title: title,
139-
subject: subject,
140-
header: header,
141-
body: body,
142-
meta: meta)
175+
subject: subjectAsData,
176+
header: headerAsData,
177+
body: bodyAsData,
178+
meta: metaAsData)
143179
}
144180
}
145181

Networking/Networking/Network/MockupNetwork.swift

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import Alamofire
66
///
77
class MockupNetwork: Network {
88

9-
/// Mapping between URL Suffix and JSON Mockup responses.
9+
/// Should this instance use the responseQueue or responseMap
10+
///
11+
private var useResponseQueue: Bool = false
12+
13+
/// Mapping between URL Suffix and JSON Mockup responses (in a FIFO queue).
14+
///
15+
private var responseQueue = [String: Queue<String>]()
16+
17+
/// Mapping between URL Suffix and JSON Mockup responses (in a simple array).
1018
///
1119
private var responseMap = [String: String]()
1220

@@ -23,17 +31,24 @@ class MockupNetwork: Network {
2331
var requestsForResponseData = [URLRequestConvertible]()
2432

2533

26-
27-
2834
/// Public Initializer
2935
///
3036
required init(credentials: Credentials) { }
3137

3238
/// Dummy convenience initializer. Remember: Real Network wrappers will allways need credentials!
3339
///
34-
convenience init() {
40+
/// Note: If the useResponseQueue param is `true`, any repsonses added via `simulateResponse` will stored in a FIFO queue
41+
/// and used once for a matching request (then removed from the queue). Subsuquent requests will use the next response in the queue, and so on.
42+
///
43+
/// If the useResponseQueue param is `false`, any repsonses added via `simulateResponse` will stored in an array and can
44+
/// be reused multiple times.
45+
///
46+
/// - Parameter useResponseQueue: Use the response queue. Default is `false`.
47+
///
48+
convenience init(useResponseQueue: Bool = false) {
3549
let dummy = Credentials(username: "", authToken: "")
3650
self.init(credentials: dummy)
51+
self.useResponseQueue = useResponseQueue
3752
}
3853

3954

@@ -48,7 +63,8 @@ class MockupNetwork: Network {
4863
return
4964
}
5065

51-
if let filename = filename(for: request), let response = Loader.jsonObject(for: filename) {
66+
let name = filename(for: request)
67+
if let name = name, let response = Loader.jsonObject(for: name) {
5268
completion(response, nil)
5369
return
5470
}
@@ -67,7 +83,8 @@ class MockupNetwork: Network {
6783
return
6884
}
6985

70-
if let filename = filename(for: request), let data = Loader.contentsOf(filename) {
86+
let name = filename(for: request)
87+
if let name = name, let data = Loader.contentsOf(name) {
7188
completion(data, nil)
7289
return
7390
}
@@ -77,15 +94,19 @@ class MockupNetwork: Network {
7794
}
7895

7996

80-
/// Public Methods
81-
///
97+
// MARK: - Public Methods
98+
//
8299
extension MockupNetwork {
83100

84101
/// Whenever a request is enqueued, we'll return the specified JSON Encoded file, whenever the Request's URL suffix matches with
85102
/// the specified one.
86103
///
87104
func simulateResponse(requestUrlSuffix: String, filename: String) {
88-
responseMap[requestUrlSuffix] = filename
105+
if useResponseQueue {
106+
addResponseToQueue(requestUrlSuffix: requestUrlSuffix, filename: filename)
107+
} else {
108+
addResponseToMap(requestUrlSuffix: requestUrlSuffix, filename: filename)
109+
}
89110
}
90111

91112
/// We'll return the specified Error, whenever a request matches the specified Suffix Criteria!
@@ -100,13 +121,43 @@ extension MockupNetwork {
100121
responseMap.removeAll()
101122
errorMap.removeAll()
102123
}
124+
}
125+
126+
127+
// MARK: - Private Helpers
128+
//
129+
private extension MockupNetwork {
130+
131+
/// Adds the URL suffix and response JSON Filename to the response queue
132+
///
133+
private func addResponseToQueue(requestUrlSuffix: String, filename: String) {
134+
if responseQueue[requestUrlSuffix] == nil {
135+
responseQueue[requestUrlSuffix] = Queue<String>()
136+
}
137+
responseQueue[requestUrlSuffix]?.enqueue(filename)
138+
}
139+
140+
/// Adds the URL suffix and response JSON Filename to the response map
141+
///
142+
private func addResponseToMap(requestUrlSuffix: String, filename: String) {
143+
responseMap[requestUrlSuffix] = filename
144+
}
103145

104-
/// Returns the Mockup JSON Filename for a given URLRequestConvertible.
146+
/// Returns the Mockup JSON Filename for a given URLRequestConvertible from either:
147+
///
148+
/// * the FIFO response queue (where the response is removed from the queue when this func returns)
149+
/// * the responseMap (array)
105150
///
106-
private func filename(for request: URLRequestConvertible) -> String? {
151+
func filename(for request: URLRequestConvertible) -> String? {
107152
let searchPath = path(for: request)
108-
for (pattern, filename) in responseMap where searchPath.hasSuffix(pattern) {
109-
return filename
153+
if useResponseQueue {
154+
if var queue = responseQueue.filter({ searchPath.hasSuffix($0.key) }).first?.value {
155+
return queue.dequeue()
156+
}
157+
} else {
158+
if let filename = responseMap.filter({ searchPath.hasSuffix($0.key) }).first?.value {
159+
return filename
160+
}
110161
}
111162

112163
return nil

Networking/NetworkingTests/Responses/notifications-load-all.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4811,6 +4811,26 @@
48114811
"id": 123456
48124812
}]
48134813
}],
4814+
"header": [{
4815+
"text": "Jorge",
4816+
"ranges": [{
4817+
"type": "user",
4818+
"indices": [0, 5],
4819+
"url": "http:\/\/www.lantean.co",
4820+
"site_id": 123456,
4821+
"email": "[email protected]",
4822+
"id": 123456
4823+
}],
4824+
"media": [{
4825+
"type": "image",
4826+
"indices": [0, 0],
4827+
"height": "256",
4828+
"width": "256",
4829+
"url": "https:\/\/gravatar.tld/some-hash"
4830+
}]
4831+
}, {
4832+
"text": "Although we already spoke elsewhere\u2026 i\u2019m NOT leaving this post without sending you a *HUGE \u2026"
4833+
}],
48144834
"body": [{
48154835
"text": "5 Likes",
48164836
"media": [{
@@ -4831,9 +4851,15 @@
48314851
}],
48324852
"meta": {
48334853
"ids": {
4854+
"user": 1234567,
4855+
"comment": 5168,
4856+
"post": 12536,
48344857
"site": 123456
48354858
},
48364859
"links": {
4860+
"user": "https:\/\/public-someurl4.sometld",
4861+
"comment": "https:\/\/public-someurl3.sometld",
4862+
"post": "https:\/\/public-someurl2.sometld",
48374863
"site": "https:\/\/public-someurl.sometld"
48384864
}
48394865
},

Storage/Storage/CoreData/CoreDataManager.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,28 @@ public class CoreDataManager: StorageManagerType {
7575
}
7676
}
7777

78+
/// Creates a new child MOC (with a private dispatch queue) whose parent is `viewStorage`.
79+
///
80+
public func newDerivedStorage() -> StorageType {
81+
let childManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
82+
childManagedObjectContext.parent = persistentContainer.viewContext
83+
childManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
84+
return childManagedObjectContext
85+
}
86+
87+
/// Saves the derived storage. Note: the closure may be called on a different thread
88+
///
89+
public func saveDerivedType(derivedStorage: StorageType, _ closure: @escaping () -> Void) {
90+
derivedStorage.perform {
91+
derivedStorage.saveIfNeeded()
92+
93+
self.viewStorage.perform {
94+
self.viewStorage.saveIfNeeded()
95+
closure()
96+
}
97+
}
98+
}
99+
78100
/// This method effectively destroys all of the stored data, and generates a blank Persistent Store from scratch.
79101
///
80102
public func reset() {

Storage/Storage/Extensions/NSManagedObjectContext+Storage.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import CocoaLumberjack
66
///
77
extension NSManagedObjectContext: StorageType {
88

9+
public var parentStorage: StorageType? {
10+
return parent
11+
}
12+
913
/// Returns all of the entities that match with a given predicate.
1014
///
1115
/// - Parameters:

Storage/Storage/Model/Note+CoreDataProperties.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ extension Note {
1717
@NSManaged public var type: String?
1818
@NSManaged public var url: String?
1919
@NSManaged public var title: String?
20-
@NSManaged public var subject: [AnyObject]?
21-
@NSManaged public var header: [AnyObject]?
22-
@NSManaged public var body: [AnyObject]?
23-
@NSManaged public var meta: AnyObject?
20+
@NSManaged public var subject: Data?
21+
@NSManaged public var header: Data?
22+
@NSManaged public var body: Data?
23+
@NSManaged public var meta: Data?
2424
}

0 commit comments

Comments
 (0)