From b49b16873b6f04593db807799c4a632959c3a938 Mon Sep 17 00:00:00 2001 From: foxsin10 Date: Mon, 25 Jul 2022 14:13:19 +0800 Subject: [PATCH 1/2] refactor: refine `RestorFile` parsing --- .../App/DApp/Model/DappScriptPayload.swift | 14 +++ .../Backup/Recovery/Model/RestoreFile.swift | 117 +++++++----------- .../Recovery/Model/WalletBackupInfo.swift | 12 +- MaskbookTests/BackupAndResotreTests.swift | 76 ++++++++++++ MaskbookTests/MaskbookTests.swift | 22 ++-- 5 files changed, 154 insertions(+), 87 deletions(-) diff --git a/Maskbook/Scene/App/DApp/Model/DappScriptPayload.swift b/Maskbook/Scene/App/DApp/Model/DappScriptPayload.swift index d668fc8b..37585cbe 100644 --- a/Maskbook/Scene/App/DApp/Model/DappScriptPayload.swift +++ b/Maskbook/Scene/App/DApp/Model/DappScriptPayload.swift @@ -38,6 +38,20 @@ struct EmptyWrapper: Codable {} struct BooleanConverted: Codable { var wrappedValue: Bool? + init(_ wrappedValue: Bool?) { + self.wrappedValue = wrappedValue + } + + init(wrapper: BooleanWrapper = .int) { + self.init(nil) + self.boolWrapper = wrapper + } + + init(_ wrappedValue: Bool?, wrapper: BooleanWrapper = .int) { + self.wrappedValue = wrappedValue + self.boolWrapper = wrapper + } + enum BooleanWrapper { case int case double diff --git a/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift b/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift index 11634469..d9a7d13e 100644 --- a/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift +++ b/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift @@ -96,7 +96,7 @@ extension RestoreFile { // MARK: Lifecycle init(from json: [String: Any]) { - _createdAt = BackupValueCompatiableFrom( + _createdAt = MaybeUInt64ToDate( json[CodingKeys.createdAt.stringValue] .flatMap { RestoreFile.converTimeInterval($0) } .flatMap { Date(timeIntervalSince1970: $0) } ?? Date() @@ -109,7 +109,7 @@ extension RestoreFile { // MARK: Internal - @BackupValueCompatiableFrom + @MaybeUInt64ToDate private(set) var createdAt: Date? let maskbookVersion: String let version: Int @@ -122,12 +122,12 @@ extension RestoreFile { // MARK: Lifecycle init(from json: [String: Any]) { - _createdAt = BackupValueCompatiableFrom( + _createdAt = MaybeUInt64ToTimeInterval( json[CodingKeys.createdAt.stringValue] .flatMap(RestoreFile.converTimeInterval) ?? 0 ) - _updatedAt = BackupValueCompatiableFrom( + _updatedAt = MaybeUInt64ToTimeInterval( json[CodingKeys.updatedAt.stringValue] .flatMap(RestoreFile.converTimeInterval) ?? 0 ) @@ -168,7 +168,7 @@ extension RestoreFile { kty = json[CodingKeys.kty.stringValue] as? String crv = json[CodingKeys.crv.stringValue] as? String key_ops = json[CodingKeys.key_ops.stringValue] as? [String] - _ext = BackupValueCompatiableFrom(json[CodingKeys.ext.stringValue] as? Bool ?? true) + _ext = BooleanConverted(json[CodingKeys.ext.stringValue] as? Bool ?? true) } // MARK: Internal @@ -177,7 +177,7 @@ extension RestoreFile { let x: String? let y: String? - @BackupValueCompatiableFrom + @BooleanConverted private(set) var ext: Bool? let key_ops: [String]? @@ -185,10 +185,10 @@ extension RestoreFile { let crv: String? } - @BackupValueCompatiableFrom + @MaybeUInt64ToTimeInterval private(set) var createdAt: TimeInterval? - @BackupValueCompatiableFrom + @MaybeUInt64ToTimeInterval private(set) var updatedAt: TimeInterval? let identifier: String? @@ -208,7 +208,7 @@ extension RestoreFile { alg = json[CodingKeys.alg.stringValue] as? String kty = json[CodingKeys.kty.stringValue] as? String key_ops = json[CodingKeys.key_ops.stringValue] as? [String] - _ext = BackupValueCompatiableFrom(json[CodingKeys.ext.stringValue] as? Bool ?? true) + _ext = BooleanConverted(json[CodingKeys.ext.stringValue] as? Bool ?? true) } // MARK: Internal @@ -219,7 +219,7 @@ extension RestoreFile { let key_ops: [String]? let kty: String? - @BackupValueCompatiableFrom + @BooleanConverted private(set) var ext: Bool? } @@ -239,13 +239,13 @@ extension RestoreFile { init(from json: [String: Any]) { path = json[CodingKeys.path.stringValue] as? String ?? "" - _withPassword = BackupValueCompatiableFrom(json[CodingKeys.withPassword.stringValue] as? Bool ?? false) + _withPassword = BooleanConverted(json[CodingKeys.withPassword.stringValue] as? Bool ?? false) } // MARK: Internal let path: String - @BackupValueCompatiableFrom + @BooleanConverted private(set) var withPassword: Bool? } @@ -259,7 +259,7 @@ extension RestoreFile { // MARK: Lifecycle init(from json: [String: Any]) { - _foundAt = BackupValueCompatiableFrom( + _foundAt = MaybeUInt64ToTimeInterval( json[CodingKeys.foundAt.stringValue] .flatMap(RestoreFile.converTimeInterval) ?? 0 ) @@ -278,7 +278,7 @@ extension RestoreFile { // MARK: Internal - @BackupValueCompatiableFrom + @MaybeUInt64ToTimeInterval private(set) var foundAt: TimeInterval? let identifier: String let postBy: String @@ -305,7 +305,7 @@ extension RestoreFile { // MARK: Lifecycle init(from json: [String: Any]) { - _time = BackupValueCompatiableFrom( + _time = MaybeUInt64ToTimeInterval( json[CodingKeys.time.stringValue] .flatMap(RestoreFile.converTimeInterval) ?? 0 ) @@ -324,7 +324,7 @@ extension RestoreFile { case group } - @BackupValueCompatiableFrom + @MaybeUInt64ToTimeInterval private(set) var time: TimeInterval? let recipientType: RecipientType let group: String? @@ -376,11 +376,11 @@ extension RestoreFile { // MARK: Lifecycle init(from json: [String: Any]) { - _createdAt = BackupValueCompatiableFrom( + _createdAt = MaybeUInt64ToTimeInterval( json[CodingKeys.createdAt.stringValue] .flatMap(RestoreFile.converTimeInterval) ?? 0 ) - _updatedAt = BackupValueCompatiableFrom( + _updatedAt = MaybeUInt64ToTimeInterval( json[CodingKeys.updatedAt.stringValue] .flatMap(RestoreFile.converTimeInterval) ?? 0 ) @@ -391,9 +391,9 @@ extension RestoreFile { // MARK: Internal - @BackupValueCompatiableFrom + @MaybeUInt64ToTimeInterval private(set) var createdAt: TimeInterval? - @BackupValueCompatiableFrom + @MaybeUInt64ToTimeInterval private(set) var updatedAt: TimeInterval? let nickName: String let identifier: String @@ -490,8 +490,8 @@ extension RestoreFile { self.type = type self.landingTxID = landingTxID key = json[CodingKeys.key.stringValue] as? String - _encrypted = BackupValueCompatiableFrom(json[CodingKeys.encrypted.stringValue] as? Bool ?? false) - _meson = BackupValueCompatiableFrom(json[CodingKeys.meson.stringValue] as? Bool ?? false) + _encrypted = BooleanConverted(json[CodingKeys.encrypted.stringValue] as? Bool) + _meson = BooleanConverted(json[CodingKeys.meson.stringValue] as? Bool) } // MARK: Internal @@ -507,9 +507,9 @@ extension RestoreFile { let type: String let landingTxID: String let key: String? - @BackupValueCompatiableFrom + @BooleanConverted var encrypted: Bool? - @BackupValueCompatiableFrom + @BooleanConverted var meson: Bool? enum CodingKeys: String, CodingKey { @@ -579,7 +579,7 @@ extension RestoreFile: RestoreItemProvider { struct BackupValueCompatiableFrom: Codable { // MARK: Lifecycle - init(_ value: Value) { + init(_ value: Value?) { wrappedValue = value } @@ -607,66 +607,39 @@ extension BackupValueCompatiableFrom where From == UInt64, Value == TimeInterval } } -extension Swift.Never: Codable { - public func encode(to encoder: Encoder) throws { - fatalError() +@propertyWrapper +struct MaybeUInt64ToTimeInterval: Codable { + var wrappedValue: TimeInterval? + init(_ wrappedValue: TimeInterval?) { + self.wrappedValue = wrappedValue } - public init(from decoder: Decoder) throws { - fatalError() - } -} - -extension BackupValueCompatiableFrom where From == Never, Value == Bool { init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - - if let stringValue = try? container.decode(String.self) { - switch stringValue.lowercased() { - case "false", "no", "0": wrappedValue = false - case "true", "yes", "1": wrappedValue = true - - default: - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Expect true/false, yes/no or 0/1 but`\(stringValue)` instead" - ) - } - } else if let intValue = try? container.decode(Int.self) { - switch intValue { - case 0: wrappedValue = false - case 1: wrappedValue = true - - default: - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Expect `0` or `1` but found `\(intValue)` instead" - ) - } - } else if let doubleValue = try? container.decode(Double.self) { - switch doubleValue { - case 0: wrappedValue = false - case 1: wrappedValue = true - - default: - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Expect `0` or `1` but found `\(doubleValue)` instead" - ) - } + if let value = try? container.decode(UInt64.self) { + wrappedValue = TimeInterval(value) / 1000 + } else if let value = try? container.decode(TimeInterval.self) { + wrappedValue = value } else { - wrappedValue = try container.decode(Bool.self) + wrappedValue = nil } } func encode(to encoder: Encoder) throws { - let rawValue = wrappedValue ?? false var container = encoder.singleValueContainer() - try container.encode(rawValue ? Int(1) : 0) + let value = (wrappedValue ?? 0) * 1000 + try container.encode(UInt64(value)) } } -extension BackupValueCompatiableFrom where From == UInt64, Value == Date { +@propertyWrapper +struct MaybeUInt64ToDate: Codable { + var wrappedValue: Date? + + init(_ wrappedValue: Date?) { + self.wrappedValue = wrappedValue + } + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let value = try? container.decode(UInt64.self) { diff --git a/Maskbook/Scene/Setting/Backup/Recovery/Model/WalletBackupInfo.swift b/Maskbook/Scene/Setting/Backup/Recovery/Model/WalletBackupInfo.swift index bdb5d679..1e177cfb 100644 --- a/Maskbook/Scene/Setting/Backup/Recovery/Model/WalletBackupInfo.swift +++ b/Maskbook/Scene/Setting/Backup/Recovery/Model/WalletBackupInfo.swift @@ -84,17 +84,17 @@ extension WalletBackupInfo { struct Parameter: Codable { let path: String // must have value, to make the situation that mnenomic exist and private key is nil reasonable - @BackupValueCompatiableFrom + @BooleanConverted var withPassword: Bool? init(path: String, withPassword: Bool = false) { self.path = path - _withPassword = BackupValueCompatiableFrom(withPassword) + _withPassword = BooleanConverted(withPassword) } init(from json: [String: Any]) { path = json[CodingKeys.path.stringValue] as? String ?? "" - _withPassword = BackupValueCompatiableFrom(json[CodingKeys.withPassword.stringValue] as? Bool ?? false) + _withPassword = BooleanConverted(json[CodingKeys.withPassword.stringValue] as? Bool ?? false) } } @@ -107,7 +107,7 @@ extension WalletBackupInfo { self.keyOps = keyOps self.kty = kty self.d = d - _ext = BackupValueCompatiableFrom(true) + _ext = BooleanConverted(true) } let crv: String @@ -116,7 +116,7 @@ extension WalletBackupInfo { let kty: String let d: String? - @BackupValueCompatiableFrom + @BooleanConverted(wrapper: .int) private(set) var ext: Bool? enum CodingKeys: String, CodingKey { @@ -133,7 +133,7 @@ extension WalletBackupInfo { kty = json[CodingKeys.kty.stringValue] as? String ?? "" crv = json[CodingKeys.crv.stringValue] as? String ?? "" keyOps = json[CodingKeys.keyOps.stringValue] as? [String] ?? [] - _ext = BackupValueCompatiableFrom(json[CodingKeys.ext.stringValue] as? Bool ?? true) + _ext = BooleanConverted(json[CodingKeys.ext.stringValue] as? Bool ?? true) } } diff --git a/MaskbookTests/BackupAndResotreTests.swift b/MaskbookTests/BackupAndResotreTests.swift index 8ce47605..31b6b900 100644 --- a/MaskbookTests/BackupAndResotreTests.swift +++ b/MaskbookTests/BackupAndResotreTests.swift @@ -121,4 +121,80 @@ class BackupAndResotreTests: XCTestCase { XCTFail("json merge failed") } } + + func testCodableDecoding() { + func boolDecoding() { + struct Meta: Codable { + @BooleanConverted + var share: Bool? + var neme: String? + } + + let json1 = #"{"share":1}"#.data(using: .utf8)! + + let result = Result { + try JSONDecoder().decode(Meta.self, from: json1) + } + + switch result { + case let .success(value): + XCTAssert(value.share == true) + case let .failure(error): + XCTFail("\(error)") + } + } + + func dateDecoding() { + struct Meta: Codable { + @MaybeUInt64ToDate + var date: Date? + } + + let json1 = #"{"date":192813313000}"#.data(using: .utf8)! + + let result = Result { + try JSONDecoder().decode(Meta.self, from: json1) + } + + switch result { + case let .success(value): + XCTAssert(value.date?.timeIntervalSince1970 == 192813313) + let encoding = try! JSONEncoder().encode(value) + let string = String(data: encoding, encoding: .utf8) + XCTAssert(string == #"{"date":192813313000}"#) + + case let .failure(error): + XCTFail("\(error)") + } + } + + func timeIntervalDecoding() { + struct Meta: Codable { + @MaybeUInt64ToTimeInterval + var time: TimeInterval? + } + + let json1 = #"{"time":192813313000}"#.data(using: .utf8)! + + let result = Result { + try JSONDecoder().decode(Meta.self, from: json1) + } + + switch result { + case let .success(value): + XCTAssert(value.time == 192813313) + + let encoding = try! JSONEncoder().encode(value) + let string = String(data: encoding, encoding: .utf8) + XCTAssert(string == #"{"time":192813313000}"#) + + case let .failure(error): + XCTFail("\(error)") + } + } + + boolDecoding() + dateDecoding() + timeIntervalDecoding() + } } diff --git a/MaskbookTests/MaskbookTests.swift b/MaskbookTests/MaskbookTests.swift index d94cabe9..06dd785a 100644 --- a/MaskbookTests/MaskbookTests.swift +++ b/MaskbookTests/MaskbookTests.swift @@ -156,19 +156,23 @@ class MaskbookTests: XCTestCase { { "title": "Six", "age": 1.8446744073709553e+19 } """.data(using: .utf8)! - guard let result = try? JSONDecoder().decode(TestModel.self, from: json) else { - XCTAssert(false) - return + let decoding = Result { + try JSONDecoder().decode(TestModel.self, from: json) } - XCTAssert(result.name.decimalNumber.decimalValue == Decimal.zero) + switch decoding { + case let .success(result): - if #available(iOS 15, *) { - XCTAssert(result.age.decimalNumber.doubleValue == 1.8446744073709553e+19) - } + if #available(iOS 15, *) { + XCTAssert(result.age.decimalNumber.doubleValue == 1.8446744073709553e+19) + } + + if #available(iOS 14, *) { + XCTAssert(result.age.decimalNumber.doubleValue == 1.8446744073709552e+19) + } - if #available(iOS 14, *) { - XCTAssert(result.age.decimalNumber.doubleValue == 1.8446744073709552e+19) + case let .failure(error): + XCTFail("decoding DecomalConverted failed \(error.localizedDescription)") } } From 3ea6ab98755e4865c7e4ea8c216841a5949d290b Mon Sep 17 00:00:00 2001 From: foxsin10 Date: Mon, 25 Jul 2022 16:11:15 +0800 Subject: [PATCH 2/2] refactor: add test case for backup file parsing --- .../Backup/Recovery/Model/RestoreFile.swift | 45 ++++++++++++++- MaskbookTests/BackupAndResotreTests.swift | 57 ++++++++++++++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift b/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift index d9a7d13e..8c01c154 100644 --- a/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift +++ b/Maskbook/Scene/Setting/Backup/Recovery/Model/RestoreFile.swift @@ -626,9 +626,11 @@ struct MaybeUInt64ToTimeInterval: Codable { } func encode(to encoder: Encoder) throws { + guard let value = wrappedValue else { + return + } var container = encoder.singleValueContainer() - let value = (wrappedValue ?? 0) * 1000 - try container.encode(UInt64(value)) + try container.encode(UInt64(value * 1000)) } } @@ -661,3 +663,42 @@ struct MaybeUInt64ToDate: Codable { } } } + +// will replace JSONDecoder().decode(_:) and JSONEncoder().encode(_:) + +extension KeyedDecodingContainer { + func decode( + _ type: MaybeUInt64ToDate.Type, + forKey key: Key + ) throws -> MaybeUInt64ToDate { + try decodeIfPresent(type, forKey: key) ?? .init(nil) + } + + func decode( + _ type: MaybeUInt64ToTimeInterval.Type, + forKey key: Key + ) throws -> MaybeUInt64ToTimeInterval { + try decodeIfPresent(type, forKey: key) ?? .init(nil) + } +} + +extension KeyedEncodingContainer { + mutating func encode( + _ value: MaybeUInt64ToDate, + forKey key: Key + ) throws { + let wrappedValue = value.wrappedValue + .flatMap { $0.timeIntervalSince1970 } + .flatMap { UInt64($0 * 1000) } + try encodeIfPresent(wrappedValue, forKey: key) + } + + mutating func encode( + _ value: MaybeUInt64ToTimeInterval, + forKey key: Key + ) throws { + if let wrappedValue = value.wrappedValue { + try encodeIfPresent(UInt64(wrappedValue * 1000), forKey: key) + } + } +} diff --git a/MaskbookTests/BackupAndResotreTests.swift b/MaskbookTests/BackupAndResotreTests.swift index 31b6b900..54aa97b2 100644 --- a/MaskbookTests/BackupAndResotreTests.swift +++ b/MaskbookTests/BackupAndResotreTests.swift @@ -150,7 +150,31 @@ class BackupAndResotreTests: XCTestCase { var date: Date? } - let json1 = #"{"date":192813313000}"#.data(using: .utf8)! + let json1 = #"{"date":1813313000}"#.data(using: .utf8)! + + let result = Result { + try JSONDecoder().decode(Meta.self, from: json1) + } + + switch result { + case let .success(value): + XCTAssert(value.date?.timeIntervalSince1970 == 1813313) + let encoding = try! JSONEncoder().encode(value) + let string = String(data: encoding, encoding: .utf8) + XCTAssert(string == #"{"date":1813313000}"#) + + case let .failure(error): + XCTFail("\(error)") + } + } + + func dateDecodingFailed() { + struct Meta: Codable { + @MaybeUInt64ToDate + var date: Date? + } + + let json1 = #"{"name":192813313000}"#.data(using: .utf8)! let result = Result { try JSONDecoder().decode(Meta.self, from: json1) @@ -158,10 +182,10 @@ class BackupAndResotreTests: XCTestCase { switch result { case let .success(value): - XCTAssert(value.date?.timeIntervalSince1970 == 192813313) + XCTAssertNil(value.date?.timeIntervalSince1970) let encoding = try! JSONEncoder().encode(value) let string = String(data: encoding, encoding: .utf8) - XCTAssert(string == #"{"date":192813313000}"#) + XCTAssert(string == #"{}"#) case let .failure(error): XCTFail("\(error)") @@ -193,8 +217,35 @@ class BackupAndResotreTests: XCTestCase { } } + func timeDecodingFailed() { + struct Meta: Codable { + @MaybeUInt64ToTimeInterval + var time: TimeInterval? + } + + let json1 = #"{"date":192813313000}"#.data(using: .utf8)! + + let result = Result { + try JSONDecoder().decode(Meta.self, from: json1) + } + + switch result { + case let .success(value): + XCTAssertNil(value.time) + + let encoding = try! JSONEncoder().encode(value) + let string = String(data: encoding, encoding: .utf8) + XCTAssert(string == #"{}"#) + + case let .failure(error): + XCTFail("\(error)") + } + } + boolDecoding() dateDecoding() + dateDecodingFailed() timeIntervalDecoding() + timeDecodingFailed() } }