Commit Graph
12 Commits
Author SHA1 Message Date
NachoSoto 9679af3bc1 CustomerInfoDecodingTests: added test to check decoding invalid data behavior (#1659)
See also #1658.
This updates the behavior of `CustomerInfoResponse` decoding so that having invalid content inside of one of the structures fails more loudly instead of getting ignored.

For that, `@DefaultValue` no longer ignores errors, and instead `@IgnoreDecodeErrors` must be used.
For some of the values in `CustomerInfoResponse` this makes sense (like `Store`), but not for things like subscriptions where we would be losing data.
2022-06-07 16:56:04 -07:00
NachoSoto 9000b5f1de Removed LossyCollections (#1657)
See #1651.

Since that regression fix, we're no longer using it because it's not possible to make it decode dictionary keys without changing their case.
Therefore we can't ensure data isn't changed in the process.
2022-06-07 15:08:59 -07:00
NachoSoto 66fb1a1fb8 DefaultDecodable.EmptyDictionary: added new test to verify partial decoding failure behavior (#1658)
The new test checks the behavior that even if only part of the dictionary fails to decode, the whole dictionary becomes empty.
2022-06-07 09:45:51 -07:00
NachoSoto 0132b69a86 Added @IgnoreEncodable property wrapper (#1567)
This allows having properties in an `Encodable` type that won't be encoded.
This will be used to storing the `rawData` when decoding our types (see #1496).
2022-05-23 18:27:18 -07:00
NachoSoto a634e493f1 Added String.asData to avoid dealing with Data? (#1566)
Turns out `Data(string.utf8)` is a much simpler way of converting `String` to `Data` that doesn't lead to an `Optional`.
UTF-8 can represent all valid Unicode code points, so this was always safe, but now we know at compile time.

The reason the old variant returned an `Optional` is because it accepts other encodings like `.ascii` which might not be able to represent everything in the `String`.

This simplifies several parts of the code that were dealing with an error that was impossible.
2022-05-20 15:53:27 -07:00
NachoSoto 56e3519ec1 Added tests for PeriodType/PurchaseOwnershipType/Store (#1558)
Follow up to #1551. Using `Codable.encodeAndDecode` we ensure that each value can be bidirectionally converted.
These tests exposed that unknown values were being encoded as `null`, but then failed to decode.

I've updated the implementation and added a few more tests to cover this behavior.
2022-05-19 11:27:12 -07:00
NachoSoto fd4b27662a Created AnyDecodable (#1568) 2022-05-16 16:12:32 -07:00
NachoSoto 3ee4d939df @LossyDictionary: workaround for decoding snake case dictionary keys (#1546)
The new test illustrates the behavior change that adding `@LossyDictionary` to a `[String: Decodable]` property had. Keys were being converted to camel case, which is inconsistent from the default behavior that `container.decode([String: Value].self, forKey: key)` would have.
The reason for this is some implementation detail on Foundation that ignores the `keyDecodingStrategy` when decoding "raw" dictionaries versus property names. To deal with this, we decode the strings first as "raw", and then match them to the corresponding converted key.

This was necessary to make sure that keys [like these](https://github.com/RevenueCat/purchases-ios/blob/4.3.0/Tests/UnitTests/Purchasing/CustomerInfoTests.swift#L58) aren't incorrectly converted to camel case, which is required for #1496.
2022-05-11 10:25:01 -07:00
NachoSoto c987f10266 Created TestCase superclass to allow re-generating snapshots (#1559)
## Motivation:

There is no way to run code statically like in Obj-C using `+ [NSObject initialize]`, so this ensures (through a new `SwiftLint` rule) that all test classes inherit this common test class instead of `XCTestCase` directly.
A future PR will make use of this new `CIRCLECI_TESTS_GENERATE_SNAPSHOTS` environment variable to allow regenerating snapshots in CI.

I've also moved the duplicated `CurrentTestCaseTracker` usages to a single point.
2022-05-10 12:40:40 -07:00
NachoSoto 5d51937eca Created LossyArray and LossyDictionary (#1543)
Inspired by https://github.com/marksands/BetterCodable/blob/master/Sources/BetterCodable but supporting nested collections and with simplified code.

These property wrappers allows decoding `Array`s and `Dictionary`s and ignore elements that fail to decode.
For example, this will ignore any elements that aren't numbers, instead of failing to decode altogether.
```swift
struct Data: Decodable {
    @LossyArray var list: [Int]
    @LossyDictionary var map: [String: Int]
}
```
This does require that the values are the right type, but these wrappers can be composed with `@DefaultDecodable.EmptyArray` and `@DefaultDecodable.EmptyDictionary` introduced in #1537 to make it produce an empty array in case of any other type error:
```swift
struct Data: Decodable {
     @DefaultDecodable.EmptyArray @LossyArray var list: [Int]
     @DefaultDecodable.EmptyDictionary @LossyDictionary var map: [String: Int]
}
```

Because of limitations of the property wrappers in Swift, an extra type `LossyArrayDictionary` allows lossy decoding of types `[String: [Decodable]`.

This will be used to vastly simplify #1496.
2022-05-10 10:35:16 -07:00
NachoSoto 0864119449 Added @IgnoreDecodeErrors property wrapper (#1541)
Follow up to #1537. This will be needed for several properties of `CustomerInfo`'s response, like `managementURL`.
2022-05-10 10:30:17 -07:00
NachoSoto 3bda51b677 Created DefaultValue and DefaultDecodable (#1537)
A future PR will make use of these: #1540.
The purpose of this is to be able to define `Decodable` types with default values without needing to implement the whole `init(from decoder: Decoder)` from scratch. If any one property has a default value, that's the only way to provide it without these.

For example, `EntitlementInfo.ProductData` implements this:
```swift
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    self.isSandbox = try container.decodeIfPresent(Bool.self, forKey: .isSandbox) ?? false
    self.originalPurchaseDate = try container.decodeIfPresent(Date.self, forKey: .originalPurchaseDate)
    self.expiresDate = try container.decodeIfPresent(Date.self, forKey: .expiresDate)
    self.unsubscribeDetectedAt = try container.decodeIfPresent(Date.self, forKey: .unsubscribeDetectedAt)
    self.billingIssuesDetectedAt = try container.decodeIfPresent(Date.self, forKey: .billingIssuesDetectedAt)
    self.periodType = container.decode(PeriodType.self, forKey: .periodType, defaultValue: .normal)
    self.store = container.decode(Store.self, forKey: .store, defaultValue: .unknownStore)
    self.ownershipType = container.decode(PurchaseOwnershipType.self,
                                          forKey: .ownershipType,
                                          defaultValue: .purchased)
}
```

With these new types, that whole implementation is unnecessary and can be data-driven instead, which is more maintanable and less error-prone:
```swift
struct ProductData: Decodable {

    @DefaultValue<PeriodType> var periodType: PeriodType
    var originalPurchaseDate: Date?
    var expiresDate: Date?
    @DefaultValue<Store> var store: Store
    @DefaultDecodable.False var isSandbox: Bool
    var unsubscribeDetectedAt: Date?
    var billingIssuesDetectedAt: Date?
    @DefaultValue<PurchaseOwnershipType> var ownershipType: PurchaseOwnershipType

}
```
2022-05-10 10:26:13 -07:00