diff --git a/Sources/BetterCodable/DefaultCodable.swift b/Sources/BetterCodable/DefaultCodable.swift index dbdcdd2..cd57289 100644 --- a/Sources/BetterCodable/DefaultCodable.swift +++ b/Sources/BetterCodable/DefaultCodable.swift @@ -1,3 +1,5 @@ +import Foundation + /// Provides a default value for missing `Decodable` data. /// /// `DefaultCodableStrategy` provides a generic strategy type that the `DefaultCodable` property wrapper can use to provide @@ -34,6 +36,8 @@ extension DefaultCodable: Equatable where Default.RawValue: Equatable { } extension DefaultCodable: Hashable where Default.RawValue: Hashable { } // MARK: - KeyedDecodingContainer +public protocol BoolCodableStrategy: DefaultCodableStrategy where RawValue == Bool {} + public extension KeyedDecodingContainer { /// Default implementation of decoding a DefaultCodable @@ -46,4 +50,31 @@ public extension KeyedDecodingContainer { return DefaultCodable(wrappedValue: P.defaultValue) } } + + /// Default implementation of decoding a `DefaultCodable` where its strategy is a `BoolCodableStrategy`. + /// + /// Tries to initially Decode a `Bool` if available, otherwise tries to decode it as an `Int` or `String` + /// when there is a `typeMismatch` decoding error. This preserves the actual value of the `Bool` in which + /// the data provider might be sending the value as different types. If everything fails defaults to + /// the `defaultValue` provided by the strategy. + func decode(_: DefaultCodable

.Type, forKey key: Key) throws -> DefaultCodable

{ + do { + let value = try decode(Bool.self, forKey: key) + return DefaultCodable(wrappedValue: value) + } catch let error { + guard let decodingError = error as? DecodingError, + case .typeMismatch = decodingError else { + return DefaultCodable(wrappedValue: P.defaultValue) + } + if let intValue = try? decodeIfPresent(Int.self, forKey: key), + let bool = Bool(exactly: NSNumber(value: intValue)) { + return DefaultCodable(wrappedValue: bool) + } else if let stringValue = try? decodeIfPresent(String.self, forKey: key), + let bool = Bool(stringValue) { + return DefaultCodable(wrappedValue: bool) + } else { + return DefaultCodable(wrappedValue: P.defaultValue) + } + } + } } diff --git a/Sources/BetterCodable/DefaultFalse.swift b/Sources/BetterCodable/DefaultFalse.swift index 5306156..bb2ea32 100644 --- a/Sources/BetterCodable/DefaultFalse.swift +++ b/Sources/BetterCodable/DefaultFalse.swift @@ -1,4 +1,4 @@ -public struct DefaultFalseStrategy: DefaultCodableStrategy { +public struct DefaultFalseStrategy: BoolCodableStrategy { public static var defaultValue: Bool { return false } } diff --git a/Sources/BetterCodable/DefaultTrue.swift b/Sources/BetterCodable/DefaultTrue.swift new file mode 100644 index 0000000..1d0bf2f --- /dev/null +++ b/Sources/BetterCodable/DefaultTrue.swift @@ -0,0 +1,8 @@ +public struct DefaultTrueStrategy: BoolCodableStrategy { + public static var defaultValue: Bool { return true } +} + +/// Decodes Bools defaulting to `true` if applicable +/// +/// `@DefaultTrue` decodes Bools and defaults the value to true if the Decoder is unable to decode the value. +public typealias DefaultTrue = DefaultCodable diff --git a/Sources/BetterCodable/LosslessValue.swift b/Sources/BetterCodable/LosslessValue.swift index 069046c..47a5880 100644 --- a/Sources/BetterCodable/LosslessValue.swift +++ b/Sources/BetterCodable/LosslessValue.swift @@ -29,9 +29,14 @@ public struct LosslessValue: Codable { func decode(_: T.Type) -> (Decoder) -> LosslessStringCodable? { return { try? T.init(from: $0) } } + + func decodeBoolFromNSNumber() -> (Decoder) -> LosslessStringCodable? { + return { (try? Int.init(from: $0)).flatMap { Bool(exactly: NSNumber(value: $0)) } } + } let types: [(Decoder) -> LosslessStringCodable?] = [ decode(String.self), + decodeBoolFromNSNumber(), decode(Bool.self), decode(Int.self), decode(Int8.self), @@ -43,8 +48,8 @@ public struct LosslessValue: Codable { decode(UInt64.self), decode(Double.self), decode(Float.self), - ] - + ] + guard let rawValue = types.lazy.compactMap({ $0(decoder) }).first, let value = T.init("\(rawValue)") diff --git a/Tests/BetterCodableTests/DefaultFalseTests.swift b/Tests/BetterCodableTests/DefaultFalseTests.swift index 1ce1ede..c0c6d56 100644 --- a/Tests/BetterCodableTests/DefaultFalseTests.swift +++ b/Tests/BetterCodableTests/DefaultFalseTests.swift @@ -37,4 +37,34 @@ class DefaultFalseTests: XCTestCase { XCTAssertEqual(fixture.truthy, true) } + + func testDecodingMisalignedBoolIntValueDecodesCorrectBoolValue() throws { + let jsonData = #"{ "truthy": 1 }"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual(fixture.truthy, true) + + let jsonData2 = #"{ "truthy": 0 }"#.data(using: .utf8)! + let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) + XCTAssertEqual(fixture2.truthy, false) + } + + func testDecodingMisalignedBoolStringValueDecodesCorrectBoolValue() throws { + let jsonData = #"{ "truthy": "true" }"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual(fixture.truthy, true) + + let jsonData2 = #"{ "truthy": "false" }"#.data(using: .utf8)! + let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) + XCTAssertEqual(fixture2.truthy, false) + } + + func testDecodingInvalidValueDecodesToDefaultValue() throws { + let jsonData = #"{ "truthy": "invalidValue" }"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual( + fixture.truthy, + false, + "Should fall in to the else block and return default value" + ) + } } diff --git a/Tests/BetterCodableTests/DefaultTrueTests.swift b/Tests/BetterCodableTests/DefaultTrueTests.swift new file mode 100644 index 0000000..9d249e4 --- /dev/null +++ b/Tests/BetterCodableTests/DefaultTrueTests.swift @@ -0,0 +1,70 @@ +import XCTest +@testable import BetterCodable + +class DefaultTrueTests: XCTestCase { + struct Fixture: Equatable, Codable { + @DefaultTrue var truthy: Bool + } + + func testDecodingFailableArrayDefaultsToFalse() throws { + let jsonData = #"{ "truthy": null }"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual(fixture.truthy, true) + } + + func testDecodingKeyNotPresentDefaultsToFalse() throws { + let jsonData = #"{}"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual(fixture.truthy, true) + } + + func testEncodingDecodedFailableArrayDefaultsToFalse() throws { + let jsonData = #"{ "truthy": null }"#.data(using: .utf8)! + var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + + _fixture.truthy = false + + let fixtureData = try JSONEncoder().encode(_fixture) + let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) + XCTAssertEqual(fixture.truthy, false) + } + + func testEncodingDecodedFulfillableBoolRetainsValue() throws { + let jsonData = #"{ "truthy": true }"#.data(using: .utf8)! + let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + let fixtureData = try JSONEncoder().encode(_fixture) + let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) + + XCTAssertEqual(fixture.truthy, true) + } + + func testDecodingMisalignedBoolIntValueDecodesCorrectBoolValue() throws { + let jsonData = #"{ "truthy": 1 }"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual(fixture.truthy, true) + + let jsonData2 = #"{ "truthy": 0 }"#.data(using: .utf8)! + let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) + XCTAssertEqual(fixture2.truthy, false) + } + + func testDecodingInvalidValueDecodesToDefaultValue() throws { + let jsonData = #"{ "truthy": "invalidValue" }"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual( + fixture.truthy, + true, + "Should fall in to the else block and return default value" + ) + } + + func testDecodingMisalignedBoolStringValueDecodesCorrectBoolValue() throws { + let jsonData = #"{ "truthy": "true" }"#.data(using: .utf8)! + let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + XCTAssertEqual(fixture.truthy, true) + + let jsonData2 = #"{ "truthy": "false" }"#.data(using: .utf8)! + let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) + XCTAssertEqual(fixture2.truthy, false) + } +} diff --git a/Tests/BetterCodableTests/LosslessValueTests.swift b/Tests/BetterCodableTests/LosslessValueTests.swift index b311a13..b8412ae 100644 --- a/Tests/BetterCodableTests/LosslessValueTests.swift +++ b/Tests/BetterCodableTests/LosslessValueTests.swift @@ -8,13 +8,13 @@ class LosslessValueTests: XCTestCase { @LosslessValue var int: Int @LosslessValue var double: Double } - + func testDecodingMisalignedTypesFromJSONTraversesCorrectType() throws { - let jsonData = #"{ "bool": "true", "string": 42, "int": "7", "double": "7.1" }"#.data(using: .utf8)! + let jsonData = #"{ "bool": "true", "string": 42, "int": "1", "double": "7.1" }"#.data(using: .utf8)! let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) XCTAssertEqual(fixture.bool, true) XCTAssertEqual(fixture.string, "42") - XCTAssertEqual(fixture.int, 7) + XCTAssertEqual(fixture.int, 1) XCTAssertEqual(fixture.double, 7.1) } @@ -43,4 +43,15 @@ class LosslessValueTests: XCTestCase { XCTAssertEqual(fixture.int, 7) XCTAssertEqual(fixture.double, 7.1) } + + func testDecodingBoolIntValueFromJSONDecodesCorrectly() throws { + let jsonData = #"{ "bool": 1, "string": "42", "int": 7, "double": 7.1 }"#.data(using: .utf8)! + let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) + let fixtureData = try JSONEncoder().encode(_fixture) + let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) + XCTAssertEqual(fixture.bool, true) + XCTAssertEqual(fixture.string, "42") + XCTAssertEqual(fixture.int, 7) + XCTAssertEqual(fixture.double, 7.1) + } }