generated from bitwarden/template
-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PM-14646: Fix JSON decoding errors (#1122)
- Loading branch information
1 parent
91a1853
commit 2a07971
Showing
10 changed files
with
292 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
BitwardenShared/Core/Platform/Utilities/DefaultValue.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import OSLog | ||
|
||
// MARK: - DefaultValueProvider | ||
|
||
/// A protocol for defining a default value for a `Decodable` type if an invalid or missing value | ||
/// is received. | ||
/// | ||
protocol DefaultValueProvider: Decodable { | ||
/// The default value to use if the value to decode is invalid or missing. | ||
static var defaultValue: Self { get } | ||
} | ||
|
||
// MARK: - DefaultValue | ||
|
||
/// A property wrapper that will default the wrapped value to a default value if decoding fails. | ||
/// This is useful for decoding types which may not be present in the response or to prevent a | ||
/// decoding failure if an invalid value is received. | ||
/// | ||
@propertyWrapper | ||
struct DefaultValue<T: DefaultValueProvider> { | ||
// MARK: Properties | ||
|
||
/// The wrapped value. | ||
let wrappedValue: T | ||
} | ||
|
||
// MARK: - Decodable | ||
|
||
extension DefaultValue: Decodable { | ||
init(from decoder: any Decoder) throws { | ||
let container = try decoder.singleValueContainer() | ||
do { | ||
wrappedValue = try container.decode(T.self) | ||
} catch { | ||
if let intValue = try? container.decode(Int.self) { | ||
Logger.application.warning( | ||
""" | ||
Cannot initialize \(T.self) from invalid Int value \(intValue, privacy: .private), \ | ||
defaulting to \(String(describing: T.defaultValue)). | ||
""" | ||
) | ||
} else if let stringValue = try? container.decode(String.self) { | ||
Logger.application.warning( | ||
""" | ||
Cannot initialize \(T.self) from invalid String value \(stringValue, privacy: .private), \ | ||
defaulting to \(String(describing: T.defaultValue)) | ||
""" | ||
) | ||
} else { | ||
Logger.application.warning( | ||
""" | ||
Cannot initialize \(T.self) from invalid unknown valid, \ | ||
defaulting to \(String(describing: T.defaultValue)) | ||
""" | ||
) | ||
} | ||
wrappedValue = T.defaultValue | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Encodable | ||
|
||
extension DefaultValue: Encodable where T: Encodable { | ||
func encode(to encoder: any Encoder) throws { | ||
var container = encoder.singleValueContainer() | ||
try container.encode(wrappedValue) | ||
} | ||
} | ||
|
||
// MARK: - Equatable | ||
|
||
extension DefaultValue: Equatable where T: Equatable {} | ||
|
||
// MARK: - Hashable | ||
|
||
extension DefaultValue: Hashable where T: Hashable {} | ||
|
||
// MARK: - KeyedDecodingContainer | ||
|
||
extension KeyedDecodingContainer { | ||
/// When decoding a `DefaultValue` wrapped value, if the property doesn't exist, default to the | ||
/// type's default value. | ||
/// | ||
/// - Parameters: | ||
/// - type: The type of value to attempt to decode. | ||
/// - key: The key used to decode the value. | ||
/// | ||
func decode<T>(_ type: DefaultValue<T>.Type, forKey key: Key) throws -> DefaultValue<T> { | ||
if let value = try decodeIfPresent(DefaultValue<T>.self, forKey: key) { | ||
return value | ||
} else { | ||
Logger.application.warning( | ||
"Missing value for \(T.self), defaulting to \(String(describing: T.defaultValue))" | ||
) | ||
return DefaultValue(wrappedValue: T.defaultValue) | ||
} | ||
} | ||
} |
79 changes: 79 additions & 0 deletions
79
BitwardenShared/Core/Platform/Utilities/DefaultValueTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import XCTest | ||
|
||
@testable import BitwardenShared | ||
|
||
class DefaultValueTests: BitwardenTestCase { | ||
// MARK: Types | ||
|
||
enum ValueType: String, Codable, DefaultValueProvider { | ||
case one, two, three | ||
|
||
static var defaultValue: ValueType { .one } | ||
} | ||
|
||
struct Model: Codable, Equatable { | ||
@DefaultValue var value: ValueType | ||
} | ||
|
||
// MARK: Tests | ||
|
||
/// `DefaultValue` encodes the wrapped value. | ||
func test_encode() throws { | ||
let subject = Model(value: .two) | ||
let data = try JSONEncoder().encode(subject) | ||
XCTAssertEqual(String(data: data, encoding: .utf8), #"{"value":"two"}"#) | ||
} | ||
|
||
/// Decoding a `DefaultValue` wrapped value will use the default value if an array cannot be | ||
/// initialized to the type. | ||
func test_decode_invalidArray() throws { | ||
let json = #"{"value": ["three"]}"# | ||
let data = try XCTUnwrap(json.data(using: .utf8)) | ||
let subject = try JSONDecoder().decode(Model.self, from: data) | ||
XCTAssertEqual(subject, Model(value: .one)) | ||
} | ||
|
||
/// Decoding a `DefaultValue` wrapped value will use the default value if an int value cannot | ||
/// be initialized to the type. | ||
func test_decode_invalidInt() throws { | ||
let json = #"{"value": 5}"# | ||
let data = try XCTUnwrap(json.data(using: .utf8)) | ||
let subject = try JSONDecoder().decode(Model.self, from: data) | ||
XCTAssertEqual(subject, Model(value: .one)) | ||
} | ||
|
||
/// Decoding a `DefaultValue` wrapped value will use the default value if a string value cannot | ||
/// be initialized to the type. | ||
func test_decode_invalidString() throws { | ||
let json = #"{"value": "unknown"}"# | ||
let data = try XCTUnwrap(json.data(using: .utf8)) | ||
let subject = try JSONDecoder().decode(Model.self, from: data) | ||
XCTAssertEqual(subject, Model(value: .one)) | ||
} | ||
|
||
/// Decoding a `DefaultValue` wrapped value will use the default value if the value is | ||
/// unknown in the JSON. | ||
func test_decode_missing() throws { | ||
let json = #"{}"# | ||
let data = try XCTUnwrap(json.data(using: .utf8)) | ||
let subject = try JSONDecoder().decode(Model.self, from: data) | ||
XCTAssertEqual(subject, Model(value: .one)) | ||
} | ||
|
||
/// Decoding a `DefaultValue` wrapped value will use the default value if the value is `null` | ||
/// in the JSON. | ||
func test_decode_null() throws { | ||
let json = #"{"value": null}"# | ||
let data = try XCTUnwrap(json.data(using: .utf8)) | ||
let subject = try JSONDecoder().decode(Model.self, from: data) | ||
XCTAssertEqual(subject, Model(value: .one)) | ||
} | ||
|
||
/// Decoding a `DefaultValue` wrapped value will decode the enum value from the JSON. | ||
func test_decode_value() throws { | ||
let json = #"{"value": "three"}"# | ||
let data = try XCTUnwrap(json.data(using: .utf8)) | ||
let subject = try JSONDecoder().decode(Model.self, from: data) | ||
XCTAssertEqual(subject, Model(value: .three)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
BitwardenShared/Core/Vault/Models/Enum/CipherRepromptTypeTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import XCTest | ||
|
||
@testable import BitwardenShared | ||
|
||
class CipherRepromptTypeTests: BitwardenTestCase { | ||
/// `defaultValue` returns the default value for the type if an invalid or missing value is | ||
/// received when decoding the type. | ||
func test_defaultValue() { | ||
XCTAssertEqual(CipherRepromptType.defaultValue, .none) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
BitwardenShared/Core/Vault/Models/SecureNoteTypeTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import XCTest | ||
|
||
@testable import BitwardenShared | ||
|
||
class SecureNoteTypeTests: BitwardenTestCase { | ||
/// `defaultValue` returns the default value for the type if an invalid or missing value is | ||
/// received when decoding the type. | ||
func test_defaultValue() { | ||
XCTAssertEqual(SecureNoteType.defaultValue, .generic) | ||
} | ||
} |