Skip to content

Commit 3a5e084

Browse files
authored
Merge pull request #6 from GoodRequest/feature/SimpleLogger
feat: Monitor/Logger support
2 parents 795727b + d66d30e commit 3a5e084

File tree

7 files changed

+236
-20
lines changed

7 files changed

+236
-20
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// Configuration.swift
3+
//
4+
//
5+
// Created by Dominik Pethö on 05/04/2024.
6+
//
7+
8+
public final class GoodPersistence {
9+
10+
/// Used for configuring the GoodPoersistance monitors
11+
public final class Configuration {
12+
13+
public static private(set) var monitors: [PersistenceMonitor] = []
14+
15+
/// Pass monitors to parameters. Each monitor invokes it's appropriate function.
16+
public static func configure(monitors: [PersistenceMonitor]) {
17+
Self.monitors = monitors
18+
}
19+
20+
}
21+
22+
}

Sources/GoodPersistence/KeychainValue.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ public enum Keychain {
7676
/// - `encodeError`: An error indicating a problem encoding data before storing it in the Keychain.
7777
public enum KeychainError: Error {
7878

79-
case accessError
80-
case decodeError
81-
case encodeError
82-
79+
case accessError(Error)
80+
case decodeError(Error)
81+
case encodeError(Error)
82+
8383
}
8484

8585
/// A property wrapper class for simplifying the storage and retrieval of Codable values in the Keychain.
@@ -156,6 +156,7 @@ public class KeychainValue<T: Codable & Equatable> {
156156
guard let data = try keychain.getData(key)
157157
else {
158158
// If no data is found in the Keychain, return the default value.
159+
PersistenceLogger.log(message: "Default keychain value [\(defaultValue)] for key [\(key)] used. Reason: Empty data.")
159160
return defaultValue
160161
}
161162
do {
@@ -166,12 +167,16 @@ public class KeychainValue<T: Codable & Equatable> {
166167
return value
167168
} catch {
168169
// Sending a failure completion event to the subject if decoding fails, and returning the default value.
169-
newSubject.send(completion: .failure(.decodeError))
170+
newSubject.send(completion: .failure(.decodeError(error)))
171+
PersistenceLogger.log(error: error)
172+
PersistenceLogger.log(message: "Default keychain value [\(defaultValue)] for key [\(key)] used. Reason: Decoding error.")
170173
return defaultValue
171174
}
172175
} catch {
173176
// Sending a failure completion event to the subject if there's an issue accessing the Keychain, and returning the default value.
174-
newSubject.send(completion: .failure(.accessError))
177+
newSubject.send(completion: .failure(.accessError(error)))
178+
PersistenceLogger.log(error: error)
179+
PersistenceLogger.log(message: "Default keychain value [\(defaultValue)] for key [\(key)] used. Reason: Keychain access.")
175180
return defaultValue
176181
}
177182
}
@@ -185,7 +190,9 @@ public class KeychainValue<T: Codable & Equatable> {
185190
try keychain.remove(key)
186191
} catch {
187192
// Sending a failure completion event to the subject if there's an issue removing the entry from the Keychain.
188-
newSubject.send(completion: .failure(.accessError))
193+
newSubject.send(completion: .failure(.accessError(error)))
194+
PersistenceLogger.log(error: error)
195+
PersistenceLogger.log(message: "Setting keychain value [\(defaultValue)] for key [\(key)] not performed. Reason: Removing from keychain failed.")
189196
return
190197
}
191198
} else {
@@ -200,18 +207,23 @@ public class KeychainValue<T: Codable & Equatable> {
200207
try keychain.set(data, key: key)
201208
} catch {
202209
// Sending a failure completion event to the subject if there's an issue storing the data in the Keychain.
203-
newSubject.send(completion: .failure(.accessError))
210+
newSubject.send(completion: .failure(.accessError(error)))
211+
PersistenceLogger.log(error: error)
212+
PersistenceLogger.log(message: "Setting keychain value [\(defaultValue)] for key [\(key)] not performed. Reason: Data encoding failed.")
204213
return
205214
}
206215
} catch {
207216
// Sending a failure completion event to the subject if there's an issue encoding the value.
208-
newSubject.send(completion: .failure(.encodeError))
217+
newSubject.send(completion: .failure(.encodeError(error)))
218+
PersistenceLogger.log(error: error)
219+
PersistenceLogger.log(message: "Setting keychain value [\(defaultValue)] for key [\(key)] not performed. Reason: Data encoding failed.")
209220
return
210221
}
211222
}
212223
// Sending the new value through the subject after successful Keychain operations.
213224
newSubject.send(newValue)
214225
subject.send(newValue)
226+
PersistenceLogger.log(message: "Keychain Data for key [\(key)] has changed to \(newValue).")
215227
}
216228
}
217229

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// PersistenceLogger.swift
3+
//
4+
//
5+
// Created by Dominik Pethö on 05/04/2024.
6+
//
7+
8+
final class PersistenceLogger {
9+
10+
static func log(error: Error) {
11+
GoodPersistence.Configuration.monitors.forEach {
12+
$0.didReceive($0, error: error)
13+
}
14+
}
15+
16+
static func log(message: String) {
17+
GoodPersistence.Configuration.monitors.forEach {
18+
$0.didReceive($0, message: message)
19+
}
20+
}
21+
22+
}
23+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// PersistenceMonitor.swift
3+
//
4+
//
5+
// Created by Dominik Pethö on 05/04/2024.
6+
//
7+
8+
/// Extend function to receive error or message from GoodPersistance library
9+
public protocol PersistenceMonitor {
10+
11+
func didReceive(_ monitor: PersistenceMonitor, error: Error)
12+
func didReceive(_ monitor: PersistenceMonitor, message: String)
13+
14+
}
15+
16+
/// Default implementation of optional protocol functions
17+
public extension PersistenceMonitor {
18+
19+
func didReceive(_ monitor: PersistenceMonitor, message: String) {}
20+
21+
}

Sources/GoodPersistence/UserDefaultsWrapper.swift

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,38 @@ public class UserDefaultValue<T: Codable> {
4040
public var wrappedValue: T {
4141
get {
4242
// If the data is of the correct type, return it.
43-
if let data = UserDefaults.standard.value(forKey: key) as? T {
43+
if let data = UserDefaults.standard.value(forKey: key) as? T {
4444
return data
4545
}
4646

4747
// If the data isn't of the correct type, try to decode it from the Data stored in UserDefaults.
48-
guard let data = UserDefaults.standard.object(forKey: key) as? Data else { return defaultValue }
49-
let value = (try? PropertyListDecoder().decode(Wrapper.self, from: data))?.value ?? defaultValue
50-
51-
return value
48+
guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
49+
PersistenceLogger.log(message: "Default UserDefaults value [\(defaultValue)] for key [\(key)] used. Reason: Data not retrieved.")
50+
return defaultValue
51+
}
52+
53+
do {
54+
let value = try PropertyListDecoder().decode(Wrapper.self, from: data).value
55+
return value
56+
} catch {
57+
PersistenceLogger.log(error: error)
58+
PersistenceLogger.log(message: "Default UserDefaults value [\(defaultValue)] for key [\(key)] used. Reason: Decoding error.")
59+
return defaultValue
60+
}
5261
}
5362
set(newValue) {
5463
// Wrap the new value in a Wrapper, and store the encoded Data in UserDefaults.
5564
let wrapper = Wrapper(value: newValue)
56-
UserDefaults.standard.set(try? PropertyListEncoder().encode(wrapper), forKey: key)
57-
58-
// Send the new value to subscribers of the subject.
59-
subject.send(newValue)
65+
66+
do {
67+
let value = try PropertyListEncoder().encode(wrapper)
68+
UserDefaults.standard.set(value, forKey: key)
69+
subject.send(newValue)
70+
PersistenceLogger.log(message: "UserDefaults data for key [\(key)] has changed to \(newValue).")
71+
} catch {
72+
PersistenceLogger.log(error: error)
73+
PersistenceLogger.log(message: "Setting UserDefaults value [\(defaultValue)] for key [\(key)] not performed. Reason: Encoding error.")
74+
}
6075
}
6176
}
6277

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Dominik Pethö on 05/04/2024.
6+
//
7+
import GoodPersistence
8+
9+
final class TestMonitor: PersistenceMonitor {
10+
11+
var error: Error?
12+
var message: String?
13+
14+
func didReceive(_ monitor: PersistenceMonitor, error: Error) {
15+
debugPrint(error)
16+
self.error = error
17+
}
18+
19+
func didReceive(_ monitor: PersistenceMonitor, message: String) {
20+
debugPrint(message)
21+
self.message = message
22+
}
23+
24+
}

Tests/GoodPersistenceTests/UserDefaultTest.swift

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ final class UserDefaultsTests: XCTestCase {
55

66
enum C {
77

8+
static let userDefaultsObjectTestMonitorKey = "Test Monitor"
89
static let firstCondition = [1]
910
static let secondCondition = [1,2]
1011
static let userDefaultsObjectKey = "numbers3"
@@ -26,9 +27,107 @@ final class UserDefaultsTests: XCTestCase {
2627

2728
}
2829

29-
override class func tearDown() {
30+
func testUserDefaultsStoresStructureIsNotRetrievedBecauseIsEmpty() {
31+
struct EmptyTest: Codable {
32+
let value: String
33+
}
34+
35+
let monitor = TestMonitor()
36+
GoodPersistence.Configuration.configure(monitors: [monitor])
37+
38+
@UserDefaultValue(C.userDefaultsObjectTestMonitorKey, defaultValue: .init(value: ""))
39+
var test: EmptyTest
40+
41+
let _ = test
42+
43+
XCTAssert(
44+
monitor.message == "Default UserDefaults value [EmptyTest(value: \"\")] for key [Test Monitor] used. Reason: Data not retrieved.",
45+
"Monitor should contain message for using default value. Contains: \(monitor.message)."
46+
)
47+
}
48+
49+
func testUserDefaultsStoresStructureDataHasChanged() {
50+
struct EmptyTest: Codable {
51+
let value: String
52+
}
53+
54+
let monitor = TestMonitor()
55+
GoodPersistence.Configuration.configure(monitors: [monitor])
56+
57+
@UserDefaultValue(C.userDefaultsObjectTestMonitorKey, defaultValue: .init(value: ""))
58+
var test: EmptyTest
59+
60+
test = .init(value: "newValue")
61+
62+
XCTAssert(
63+
monitor.message == "UserDefaults data for key [Test Monitor] has changed to EmptyTest(value: \"newValue\").",
64+
"Monitor should contain message for using default value. Contains: \(monitor.message)."
65+
)
66+
}
67+
68+
func testMessageForUserDefaultsStoresStructureIsNotDecodedCorrectly() {
69+
struct EmptyTest: Codable {
70+
let value: String
71+
}
72+
73+
struct EmptyTestFailure: Codable {
74+
let value: String
75+
let secondValue: String
76+
}
77+
78+
let monitor = TestMonitor()
79+
GoodPersistence.Configuration.configure(monitors: [monitor])
80+
81+
@UserDefaultValue(C.userDefaultsObjectTestMonitorKey, defaultValue: .init(value: ""))
82+
var test: EmptyTest
83+
test = .init(value: "value")
84+
85+
@UserDefaultValue(C.userDefaultsObjectTestMonitorKey, defaultValue: .init(value: "", secondValue: ""))
86+
var testFailure: EmptyTestFailure
87+
let _ = testFailure
88+
89+
XCTAssert(
90+
monitor.message == "Default UserDefaults value [EmptyTestFailure(value: \"\", secondValue: \"\")] for key [Test Monitor] used. Reason: Decoding error.",
91+
"Monitor should contain message for using default value. Contains: \(monitor.message)."
92+
)
93+
94+
XCTAssert(
95+
monitor.error != nil,
96+
"Monitor should contain error for using default value. Contains error: \(monitor.error)."
97+
)
98+
}
99+
100+
func testErrorForDefaultsStoresStructureIsNotDecodedCorrectly() {
101+
struct EmptyTest: Codable {
102+
let value: String
103+
}
104+
105+
struct EmptyTestFailure: Codable {
106+
let value: String
107+
let secondValue: String
108+
}
109+
110+
let monitor = TestMonitor()
111+
GoodPersistence.Configuration.configure(monitors: [monitor])
112+
113+
@UserDefaultValue(C.userDefaultsObjectTestMonitorKey, defaultValue: .init(value: ""))
114+
var test: EmptyTest
115+
test = .init(value: "value")
116+
117+
@UserDefaultValue(C.userDefaultsObjectTestMonitorKey, defaultValue: .init(value: "", secondValue: ""))
118+
var testFailure: EmptyTestFailure
119+
let _ = testFailure
120+
121+
XCTAssert(
122+
monitor.error != nil,
123+
"Monitor should contain error for using default value. Contains error: \(monitor.error)."
124+
)
125+
}
126+
127+
override func tearDown() {
30128
UserDefaults.standard.removeObject(forKey: C.userDefaultsObjectKey)
31-
UserDefaults.standard.synchronize()
129+
UserDefaults.standard.removeObject(forKey: C.userDefaultsObjectTestMonitorKey)
130+
GoodPersistence.Configuration.configure(monitors: [])
32131
}
33132

34133
}

0 commit comments

Comments
 (0)