Skip to content

Created @LossyArray and @LossyDictionary#1543

Merged
NachoSoto merged 1 commit into
mainfrom
ignore-decode-errors-collections
May 10, 2022
Merged

Created @LossyArray and @LossyDictionary#1543
NachoSoto merged 1 commit into
mainfrom
ignore-decode-errors-collections

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Apr 26, 2022

Copy link
Copy Markdown
Contributor

Inspired by https://github.com/marksands/BetterCodable/blob/master/Sources/BetterCodable but supporting nested collections and with simplified code.

These property wrappers allows decoding Arrays and Dictionarys and ignore elements that fail to decode.
For example, this will ignore any elements that aren't numbers, instead of failing to decode altogether.

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:

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 NachoSoto changed the title [skip ci] [WIP] Started LossyArray and LossyDictionary [WIP] Started LossyArray and LossyDictionary Apr 26, 2022
@NachoSoto NachoSoto force-pushed the ignore-decode-errors branch from 177e79e to ba4dadf Compare April 26, 2022 23:58
@NachoSoto NachoSoto force-pushed the ignore-decode-errors-collections branch 6 times, most recently from 02a2b09 to db374f2 Compare April 27, 2022 01:11
Comment on lines 223 to 241

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.

These tests check that both property wrappers can be composed.

@NachoSoto NachoSoto requested a review from a team April 27, 2022 01:12
@NachoSoto NachoSoto changed the title [WIP] Started LossyArray and LossyDictionary Created LossyArray and LossyDictionary Apr 27, 2022
@NachoSoto NachoSoto marked this pull request as ready for review April 27, 2022 01:12
@NachoSoto NachoSoto force-pushed the ignore-decode-errors-collections branch from db374f2 to 1819dc6 Compare April 27, 2022 01:14
@NachoSoto NachoSoto changed the title Created LossyArray and LossyDictionary Created @LossyArray and @LossyDictionary Apr 27, 2022
@NachoSoto NachoSoto force-pushed the ignore-decode-errors branch from ba4dadf to 258721e Compare April 28, 2022 21:26
@NachoSoto NachoSoto force-pushed the ignore-decode-errors-collections branch from 1819dc6 to f7315e1 Compare April 28, 2022 21:26
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.

@joshdholtz joshdholtz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks awesome 😍 A few small questions but otherwise this looks 💯 and good to merge!

@propertyWrapper
struct LossyArrayDictionary<Value: Decodable> {

var wrappedValue: [String: [Value]]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it safe to assume the key will always be a String? I mean, I can't see why it wouldn't be in any of our cases but not sure if that something we want to open up 🤷‍♂️

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.

Good question. Swift's CodingKey allows keys to be String or Int, in a way that Integer keys can be implicitly converted to String when decoding JSON dictionaries. We don't use that, so for simplicity these property wrappers assume just String, but it would be trivial to change these by adding a CodingKey associatedtype so that integer keys are supported as well.


class DecoderExtensionsLossyCollectionTests: XCTestCase {

private struct Data: Codable, Equatable {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this needs to be changed but I was confused at how Data was being used thinking that all the Data objects in the tests were the NSData type of Data objects and then not realizing there is a custom Data struct here.

I would also name this something like TestData or something but that is just kind of a personal nit 🙃

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.

Well this isn't just Data (as in the global namespace), it's DecoderExtensionsLossyCollectionTests.Data (it's namespaced inside the test class).

So in a way, it is like "Test Data". Does that make sense?

@NachoSoto NachoSoto force-pushed the ignore-decode-errors branch from 258721e to d93db85 Compare May 10, 2022 17:29
Base automatically changed from ignore-decode-errors to main May 10, 2022 17:30
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 NachoSoto force-pushed the ignore-decode-errors-collections branch from f7315e1 to 10b1caf Compare May 10, 2022 17:31
@NachoSoto NachoSoto merged commit 5d51937 into main May 10, 2022
@NachoSoto NachoSoto deleted the ignore-decode-errors-collections branch May 10, 2022 17:35
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