From be7e30ba366a7a10e198671505dbe6b4e07b76bc Mon Sep 17 00:00:00 2001 From: hank121314 Date: Fri, 28 Oct 2022 16:27:17 +0800 Subject: [PATCH] Support serializing and deserializing nested custom types (#118) --- Sources/Defaults/Utilities.swift | 32 +++++--- .../DefaultsCustomBridgeTests.swift | 75 ++++++++++++++++++- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index 452378e..682bc6f 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -167,18 +167,20 @@ extension Defaults.Serializable { return Value.toValue(anyObject) ``` */ - static func toValue(_ anyObject: Any) -> Self? { - // Return directly if `anyObject` can cast to Value, since it means `Value` is a natively supported type. + static func toValue(_ anyObject: Any, type: T.Type = Self.self) -> T? { if - isNativelySupportedType, - let anyObject = anyObject as? Self + T.isNativelySupportedType, + let anyObject = anyObject as? T { return anyObject - } else if let value = bridge.deserialize(anyObject as? Serializable) { - return value as? Self } - return nil + guard let nextType = T.Serializable.self as? any Defaults.Serializable.Type else { + // This is a special case for the types which do not conform to `Defaults.Serializable` (for example, `Any`). + return T.bridge.deserialize(anyObject as? T.Serializable) as? T + } + + return T.bridge.deserialize(toValue(anyObject, type: nextType) as? T.Serializable) as? T } /** @@ -190,14 +192,20 @@ extension Defaults.Serializable { set(Value.toSerialize(value), forKey: key) ``` */ - static func toSerializable(_ value: Self) -> Any? { - // Return directly if `Self` is a natively supported type, since it does not need serialization. - if isNativelySupportedType { + static func toSerializable(_ value: T) -> Any? { + if T.isNativelySupportedType { return value - } else if let serialized = bridge.serialize(value as? Value) { + } + + guard let serialized = T.bridge.serialize(value as? T.Value) else { + return nil + } + + guard let next = serialized as? any Defaults.Serializable else { + // This is a special case for the types which do not conform to `Defaults.Serializable` (for example, `Any`). return serialized } - return nil + return toSerializable(next) } } diff --git a/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift b/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift index e8653f8..f35df79 100644 --- a/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift +++ b/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift @@ -38,13 +38,57 @@ public final class DefaultsUserBridge: Defaults.Bridge { private let fixtureCustomBridge = User(username: "hank121314", password: "123456") +struct PlainHourMinuteTimeRange: Hashable, Codable { + var start: PlainHourMinuteTime + var end: PlainHourMinuteTime +} + +extension PlainHourMinuteTimeRange: Defaults.Serializable { + struct Bridge: Defaults.Bridge { + typealias Value = PlainHourMinuteTimeRange + typealias Serializable = [PlainHourMinuteTime] + + public func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return [value.start, value.end] + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard + let array = object, + let start = array[safe: 0], + let end = array[safe: 1] + else { + return nil + } + + return .init(start: start, end: end) + } + } + + static let bridge = Bridge() +} + +struct PlainHourMinuteTime: Hashable, Codable, Defaults.Serializable { + var hour: Int + var minute: Int +} + +extension Collection { + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } +} + extension Defaults.Keys { fileprivate static let customBridge = Key("customBridge", default: fixtureCustomBridge) fileprivate static let customBridgeArray = Key<[User]>("array_customBridge", default: [fixtureCustomBridge]) fileprivate static let customBridgeDictionary = Key<[String: User]>("dictionary_customBridge", default: ["0": fixtureCustomBridge]) } - final class DefaultsCustomBridge: XCTestCase { override func setUp() { super.setUp() @@ -148,6 +192,35 @@ final class DefaultsCustomBridge: XCTestCase { XCTAssertEqual(Defaults[key]["0"]?[1], fixtureCustomBridge) } + func testRecursiveKey() { + let start = PlainHourMinuteTime(hour: 1, minute: 0) + let end = PlainHourMinuteTime(hour: 2, minute: 0) + let range = PlainHourMinuteTimeRange(start: start, end: end) + let key = Defaults.Key("independentCustomBridgeRecursiveKey", default: range) + XCTAssertEqual(Defaults[key].start.hour, range.start.hour) + XCTAssertEqual(Defaults[key].start.minute, range.start.minute) + XCTAssertEqual(Defaults[key].end.hour, range.end.hour) + XCTAssertEqual(Defaults[key].end.minute, range.end.minute) + guard let rawValue = UserDefaults.standard.array(forKey: key.name) as? [String] else { + XCTFail("rawValue should not be nil") + return + } + XCTAssertEqual(rawValue, [#"{"minute":0,"hour":1}"#, #"{"minute":0,"hour":2}"#]) + let next_start = PlainHourMinuteTime(hour: 3, minute: 58) + let next_end = PlainHourMinuteTime(hour: 4, minute: 59) + let next_range = PlainHourMinuteTimeRange(start: next_start, end: next_end) + Defaults[key] = next_range + XCTAssertEqual(Defaults[key].start.hour, next_range.start.hour) + XCTAssertEqual(Defaults[key].start.minute, next_range.start.minute) + XCTAssertEqual(Defaults[key].end.hour, next_range.end.hour) + XCTAssertEqual(Defaults[key].end.minute, next_range.end.minute) + guard let nextRawValue = UserDefaults.standard.array(forKey: key.name) as? [String] else { + XCTFail("nextRawValue should not be nil") + return + } + XCTAssertEqual(nextRawValue, [#"{"minute":58,"hour":3}"#, #"{"minute":59,"hour":4}"#]) + } + func testType() { XCTAssertEqual(Defaults[.customBridge], fixtureCustomBridge) let newUser = User(username: "sindresorhus", password: "123456789")