Skip to content

Created DefaultValue and DefaultDecodable#1537

Merged
NachoSoto merged 2 commits into
mainfrom
default-value-decoder
May 10, 2022
Merged

Created DefaultValue and DefaultDecodable#1537
NachoSoto merged 2 commits into
mainfrom
default-value-decoder

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Apr 26, 2022

Copy link
Copy Markdown
Contributor

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:

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:

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

}

@NachoSoto NachoSoto requested a review from a team April 26, 2022 16:37
@NachoSoto NachoSoto force-pushed the default-value-decoder branch 2 times, most recently from 5890179 to 4a7dc15 Compare April 26, 2022 16:53
Comment thread Sources/FoundationExtensions/Decoder+Extensions.swift Outdated
Comment on lines 84 to 90

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically KeyedDecodingContainer.decode(_:forKey:defaultValue:), but the default value is provided statically.

@NachoSoto NachoSoto force-pushed the default-value-decoder branch from 918f6cf to ff8f417 Compare April 26, 2022 18:11
NachoSoto added a commit that referenced this pull request Apr 26, 2022
Follow up to #1537. This will be needed for several properties of `CustomerInfo`'s response, like `managementURL`.
@NachoSoto NachoSoto force-pushed the default-value-decoder branch from ff8f417 to 3a9ec92 Compare April 26, 2022 23:36
@NachoSoto NachoSoto force-pushed the default-value-decoder branch from 3a9ec92 to f7a3452 Compare April 26, 2022 23:56
NachoSoto added a commit that referenced this pull request Apr 26, 2022
NachoSoto added a commit that referenced this pull request Apr 26, 2022
Follow up to #1537. This will be needed for several properties of `CustomerInfo`'s response, like `managementURL`.
NachoSoto added a commit that referenced this pull request Apr 27, 2022
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]`.
NachoSoto added a commit that referenced this pull request Apr 27, 2022
NachoSoto added a commit that referenced this pull request Apr 28, 2022
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]`.
NachoSoto added a commit that referenced this pull request Apr 28, 2022
NachoSoto added a commit that referenced this pull request Apr 28, 2022
- Removed all custom deserialization code thanks to #1537, #1541, and #1543.
- The format of the customer info response is now very concise and only in one place (`CustomerInfoResponse`) instead of being passed through dictionaries and magic strings everywhere.
- `CustomerInfo` was combining the dictionaries inside of `"subscriptions"` and `"non_subscriptions"`, despite them having different formats. This is now made explicit through type conversions between the two, (see `CustomerInfoResponse.allTransactionsByProductId`).
- Removed all custom `CustomerInfo` errors since deserialization is automatic, and the underlying `Error` information will be provided thanks to `ErrorUtils.logDecodingError`.
- `CustomerInfo` deserialization always wraps errors into `ErrorCode.customerInfoError`, which is now also covered in a new test.
- Added a new snapshot test that covers `CustomerInfo` serialization by storing the result in a JSON file.

- `CustomerInfo` equality is now automatic through the `CustomerInfoResponse` `Equatable` conformance.
- Simplified `schemaVersion` handling
- All tests still use `CustomerInfo(data: [String: Any])`, but that method is only visible for tests now, and it uses the underlying `Decodable` implementation (see `CustomerInfo+TestExtensions.swift`).
- Added extra tests for `CustomerInfoResponse` that use a fixture `CustomerInfo.json`, which is easier to maintain than having JSON in code.

- Improved definition of the protocol so that our types only have to implement one method:
```swift
extension CustomerInfo: RawDataContainer {
    @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
    public var underlyingData: some Encodable {
        return self.data.response
    }
}
```

- The protocol will provide the `rawData` public method through the default implementation automatically.
- The only downside is that this needs to be iOS 13+ only because of `some Encodable`, which allows us to implement this without needing to leak the underlying type.
@NachoSoto NachoSoto force-pushed the default-value-decoder branch from a9b4a53 to 4d23d75 Compare May 10, 2022 15:49
NachoSoto added 2 commits May 10, 2022 08:50
A future PR will make use of these.
The purpose of these 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

}
```
@NachoSoto NachoSoto force-pushed the default-value-decoder branch from 4d23d75 to e0b26ce Compare May 10, 2022 15:50
@NachoSoto NachoSoto merged commit 3bda51b into main May 10, 2022
@NachoSoto NachoSoto deleted the default-value-decoder branch May 10, 2022 17:26
NachoSoto added a commit that referenced this pull request May 10, 2022
NachoSoto added a commit that referenced this pull request May 10, 2022
…rty wrappers (#1540)

Follow up to #1537.
`EntitlementInfo.ProductData` and related types will be moved out as part of #1496, to become part of the `CustomerInfo` response. But this is an intermediate change to simplify the decoding of this part of the data.
NachoSoto added a commit that referenced this pull request May 10, 2022
Follow up to #1537. This will be needed for several properties of `CustomerInfo`'s response, like `managementURL`.
NachoSoto added a commit that referenced this pull request May 10, 2022
Follow up to #1537. This will be needed for several properties of `CustomerInfo`'s response, like `managementURL`.
NachoSoto added a commit that referenced this pull request May 10, 2022
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]`.
NachoSoto added a commit that referenced this pull request May 10, 2022
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.
NachoSoto added a commit that referenced this pull request May 12, 2022
- Removed all custom deserialization code thanks to #1537, #1541, and #1543.
- The format of the customer info response is now very concise and only in one place (`CustomerInfoResponse`) instead of being passed through dictionaries and magic strings everywhere.
- `CustomerInfo` was combining the dictionaries inside of `"subscriptions"` and `"non_subscriptions"`, despite them having different formats. This is now made explicit through type conversions between the two, (see `CustomerInfoResponse.allTransactionsByProductId`).
- Removed all custom `CustomerInfo` errors since deserialization is automatic, and the underlying `Error` information will be provided thanks to `ErrorUtils.logDecodingError`.
- `CustomerInfo` deserialization always wraps errors into `ErrorCode.customerInfoError`, which is now also covered in a new test.
- Added a new snapshot test that covers `CustomerInfo` serialization by storing the result in a JSON file.

- `CustomerInfo` equality is now automatic through the `CustomerInfoResponse` `Equatable` conformance.
- Simplified `schemaVersion` handling
- All tests still use `CustomerInfo(data: [String: Any])`, but that method is only visible for tests now, and it uses the underlying `Decodable` implementation (see `CustomerInfo+TestExtensions.swift`).
- Added extra tests for `CustomerInfoResponse` that use a fixture `CustomerInfo.json`, which is easier to maintain than having JSON in code.

- Improved definition of the protocol so that our types only have to implement one method:
```swift
extension CustomerInfo: RawDataContainer {
    @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
    public var underlyingData: some Encodable {
        return self.data.response
    }
}
```

- The protocol will provide the `rawData` public method through the default implementation automatically.
- The only downside is that this needs to be iOS 13+ only because of `some Encodable`, which allows us to implement this without needing to leak the underlying type.
NachoSoto added a commit that referenced this pull request May 20, 2022
- Removed all custom deserialization code thanks to #1537, #1541, and #1543.
- The format of the customer info response is now very concise and only in one place (`CustomerInfoResponse`) instead of being passed through dictionaries and magic strings everywhere.
- `CustomerInfo` was combining the dictionaries inside of `"subscriptions"` and `"non_subscriptions"`, despite them having different formats. This is now made explicit through type conversions between the two, (see `CustomerInfoResponse.allTransactionsByProductId`).
- Removed all custom `CustomerInfo` errors since deserialization is automatic, and the underlying `Error` information will be provided thanks to `ErrorUtils.logDecodingError`.
- `CustomerInfo` deserialization always wraps errors into `ErrorCode.customerInfoError`, which is now also covered in a new test.
- Added a new snapshot test that covers `CustomerInfo` serialization by storing the result in a JSON file.

- `CustomerInfo` equality is now automatic through the `CustomerInfoResponse` `Equatable` conformance.
- Simplified `schemaVersion` handling
- All tests still use `CustomerInfo(data: [String: Any])`, but that method is only visible for tests now, and it uses the underlying `Decodable` implementation (see `CustomerInfo+TestExtensions.swift`).
- Added extra tests for `CustomerInfoResponse` that use a fixture `CustomerInfo.json`, which is easier to maintain than having JSON in code.

- Improved definition of the protocol so that our types only have to implement one method:
```swift
extension CustomerInfo: RawDataContainer {
    @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
    public var underlyingData: some Encodable {
        return self.data.response
    }
}
```

- The protocol will provide the `rawData` public method through the default implementation automatically.
- The only downside is that this needs to be iOS 13+ only because of `some Encodable`, which allows us to implement this without needing to leak the underlying type.
NachoSoto added a commit that referenced this pull request May 24, 2022
- Removed all custom deserialization code thanks to #1537, #1541, and #1543.
- The format of the customer info response is now very concise and only in one place (`CustomerInfoResponse`) instead of being passed through dictionaries and magic strings everywhere.
- `CustomerInfo` was combining the dictionaries inside of `"subscriptions"` and `"non_subscriptions"`, despite them having different formats. This is now made explicit through type conversions between the two, (see `CustomerInfoResponse.allTransactionsByProductId`).
- Removed all custom `CustomerInfo` errors since deserialization is automatic, and the underlying `Error` information will be provided thanks to `ErrorUtils.logDecodingError`.
- `CustomerInfo` deserialization always wraps errors into `ErrorCode.customerInfoError`, which is now also covered in a new test.
- Added a new snapshot test that covers `CustomerInfo` serialization by storing the result in a JSON file.

- `CustomerInfo` equality is now automatic through the `CustomerInfoResponse` `Equatable` conformance.
- Simplified `schemaVersion` handling
- All tests still use `CustomerInfo(data: [String: Any])`, but that method is only visible for tests now, and it uses the underlying `Decodable` implementation (see `CustomerInfo+TestExtensions.swift`).
- Added extra tests for `CustomerInfoResponse` that use a fixture `CustomerInfo.json`, which is easier to maintain than having JSON in code.

- Improved definition of the protocol so that our types only have to implement one method:
```swift
extension CustomerInfo: RawDataContainer {
    @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
    public var underlyingData: some Encodable {
        return self.data.response
    }
}
```

- The protocol will provide the `rawData` public method through the default implementation automatically.
- The only downside is that this needs to be iOS 13+ only because of `some Encodable`, which allows us to implement this without needing to leak the underlying type.
NachoSoto added a commit that referenced this pull request May 24, 2022
### Depends on:
- #1537
- #1540
- #1541
- #1543
- #1546
- #1547
- #1550
- #1551,
- #1565

### Changes

- Removed all custom deserialization code thanks to #1537, #1541, and #1543.
- The format of the customer info response is now very concise and only in one place (`CustomerInfoResponse`) instead of being passed through dictionaries and magic strings everywhere.
- `CustomerInfo` was combining the dictionaries inside of `"subscriptions"` and `"non_subscriptions"`, despite them having different formats. This is now made explicit through type conversions between the two, (see `CustomerInfoResponse.allTransactionsByProductId`).
- Removed all custom `CustomerInfo` errors since deserialization is automatic, and the underlying `Error` information will be provided thanks to `ErrorUtils.logDecodingError`.
- `CustomerInfo` deserialization always wraps errors into `ErrorCode.customerInfoError`, which is now also covered in a new test.
- Added a new snapshot test that covers `CustomerInfo` serialization by storing the result in a JSON file.

### Other changes:
- `CustomerInfo` equality is now automatic through the `CustomerInfoResponse` `Equatable` conformance.
- Simplified `schemaVersion` handling
- All tests still use `CustomerInfo(data: [String: Any])`, but that method is only visible for tests now, and it uses the underlying `Decodable` implementation (see `CustomerInfo+TestExtensions.swift`).
- Added extra tests for `CustomerInfoResponse` that use a fixture `CustomerInfo.json`, which is easier to maintain than having JSON in code.
- `RawDataContainer` implementation is changed in #1565.

### TODO:
- [x] Fix all tests
- [x] Update `RawValueContainer`
- [x] Consider recovering `CustomerInfoError`
- [x] Look at all previously logged errors
- [x] Finish `CustomerInfoResponseTests`
- [x] Handle all TODOs
- [x] Make `RawDataContainer` actually store all the original data: #1565
@joshdholtz joshdholtz mentioned this pull request May 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants