This allows decorating a `String` property in a `Decodable` type so it
ensures it gets converted to `nil` if it's empty.
It will be used for `PaywallData`.
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.
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.
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).
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.
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.
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.
## 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.
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.
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
}
```