Skip to content

Commit

Permalink
Test cases for LosslessValue Bool Int value (#28)
Browse files Browse the repository at this point in the history
* Test cases for LosslessValue Bool Int value

* DefaultFalse solution

* Add DefaultTrue case

* Add small comment

* Intercept boolean numbers in LosslessValue

* Clean up LosslessValue test cases

* Add documentation to BoolCodableStrategy decoding

* Remove comment

* Fix incorrect documentation

* Add String misalignment test case

* Add test cases for DefaultTrue

* Change DefaultFalse and DefaultTrue test names

* Add invalid value test case for DefaultFalse

* Add LosslessValue test for Bool edge case

Co-authored-by: Mark Sands <[email protected]>
  • Loading branch information
serjooo and marksands authored Sep 28, 2020
1 parent 313471c commit 866a1f2
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 6 deletions.
31 changes: 31 additions & 0 deletions Sources/BetterCodable/DefaultCodable.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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<P: BoolCodableStrategy>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
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)
}
}
}
}
2 changes: 1 addition & 1 deletion Sources/BetterCodable/DefaultFalse.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public struct DefaultFalseStrategy: DefaultCodableStrategy {
public struct DefaultFalseStrategy: BoolCodableStrategy {
public static var defaultValue: Bool { return false }
}

Expand Down
8 changes: 8 additions & 0 deletions Sources/BetterCodable/DefaultTrue.swift
Original file line number Diff line number Diff line change
@@ -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<DefaultTrueStrategy>
9 changes: 7 additions & 2 deletions Sources/BetterCodable/LosslessValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ public struct LosslessValue<T: LosslessStringCodable>: Codable {
func decode<T: LosslessStringCodable>(_: 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),
Expand All @@ -43,8 +48,8 @@ public struct LosslessValue<T: LosslessStringCodable>: Codable {
decode(UInt64.self),
decode(Double.self),
decode(Float.self),
]
]

guard
let rawValue = types.lazy.compactMap({ $0(decoder) }).first,
let value = T.init("\(rawValue)")
Expand Down
30 changes: 30 additions & 0 deletions Tests/BetterCodableTests/DefaultFalseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
}
}
70 changes: 70 additions & 0 deletions Tests/BetterCodableTests/DefaultTrueTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
17 changes: 14 additions & 3 deletions Tests/BetterCodableTests/LosslessValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
}

0 comments on commit 866a1f2

Please sign in to comment.