Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAPSIOS-118 Implement DictionaryEncoder for Layer properties. #1476

Merged
merged 11 commits into from
Aug 3, 2022

Conversation

maios
Copy link
Contributor

@maios maios commented Jul 19, 2022

  • Encode Layer properties directly to a dictionary, instead of encoding to data using JSONEncoder and then serializing the data to a JSON dictionary.
  • Support encoding null values based on encoding context.
  • When updating a layer, merge the old properties to new properties, if a property is reset (old is non-nil and new is nil) then we will use the default value.

NOTE: This will depend on the upstream PR https://github.com/mapbox/mapbox-gl-native-internal/pull/3736 in order to support passing null StyleTransition

Pull request checklist:

  • Write tests for all new functionality. If tests were not written, please explain why.
  • Add documentation comments for any added or updated public APIs.
  • Describe the changes in this PR, especially public API changes.
  • Add a changelog entry to to bottom of the relevant section (typically the ## main heading near the top).

- Encode Layer properties directly to a dictionary, instead of encoding to data using JSONEncoder and then serializing the data to a JSON dictionary.
- Support encoding null values based on encoding context.
@maios maios marked this pull request as ready for review July 26, 2022 15:27
@maios maios requested a review from a team as a code owner July 26, 2022 15:27
Copy link
Contributor

@evil159 evil159 left a comment

Choose a reason for hiding this comment

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

Very interesting to see a custom implementation of Swift's encoders and decoders 👍


let layoutProperties = try XCTUnwrap(rootProperties["layout"] as? [String: Any])
XCTAssertEqual(layoutProperties["visibility"] as? String, "visible", "visibility is not reset and should keep old value")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Check for final layer values to be as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it does not work with the test setup like this, because we would need to get a dictionary of layer's properties then decode it to a Layer, so we could only verify that when we call Style.updateLayer it will invoke its StyleManager with the expected parameters, which is a Dictionary of merged old properties and new properties.

Copy link
Contributor

@evil159 evil159 Jul 27, 2022

Choose a reason for hiding this comment

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

Can we have integration tests for this?

}

func encodeIfPresent<T: Encodable>(_ value: T?, forKey key: Key) throws {
let shouldEncodeNilValue = userInfo[.shouldEncodeNilValues] as? Bool ?? false
Copy link
Contributor

Choose a reason for hiding this comment

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

With shouldEncodeNilValues baked into the source it'll be more clear to expose it as a property on DictionaryEncoder rather than user info key.
Primary use case for userInfo is for the user of the API to pass some contextual info down the line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point, this is done now!

}
}

func encodeIfPresent<T: Encodable>(_ value: T?, forKey key: Key) throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that something is wrong with this - this method does not seem to be called for primitive types(like Double - e.g. minzoom is not encoded).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

my bad, I didn't commit encodeIfPresent for primitive types :-D

["value": 10]
)
XCTAssertEqual(
try sut.encode(DummyNilable<Int>(value: nil)) as? [String: Int?],
Copy link
Contributor

Choose a reason for hiding this comment

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

There's some funky stuff going on - this works with a generic struct. But when using a similar specialized struct - the result is an empty dictionary.

    private struct DummyFloatEncodable: Encodable {
        let value: Float?
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a sure way to test these would be to define a dedicated struct for every test 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, I am also thinking of using stylegen to generate the tests case for each Layer's struct :-D let's see!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test is expected the dictionary to be empty though? Did you mean to comment this in other places?

@@ -344,4 +344,44 @@ final class StyleTests: XCTestCase {
bounds: CoordinateBounds(southwest: .random(), northeast: .random()))
)
}

func testStyleCanUpdateLayer() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add tests that check that:

  • nilling out unmodified properties works correctly
  • updating a property value that already has a value works correctly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

Comment on lines 25 to 28
T.self == Data.self || T.self == NSData.self ||
T.self == Date.self || T.self == NSDate.self ||
T.self == Decimal.self || T.self == NSDecimalNumber.self ||
T.self == URL.self || T.self == NSURL.self
Copy link
Contributor

Choose a reason for hiding this comment

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

What does it mean that only these types are supported?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

aye, the function name should be changed into something that makes more sense, but this is to check for the type and encode them properly (maybe later will be based on configuration i.e data will be encoded with base64, date will be encoded with ISO8601 etc)
I will rename the function.

- Add shouldEncodeNilValues as a property of DictionaryEncoder.
- Fix encodeIfPresent for primitive types when a container types is not generic and update unit tests.
- Update Data, Date, Decimal and URL encoding to match the default behavior of JSONEncoder.
@maios maios requested review from evil159 and a team July 27, 2022 16:45
}

func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {
try encodeIfPresent(NilEncodable(value), forKey: key)
Copy link
Contributor

Choose a reason for hiding this comment

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

What does wrapping the value into NilEncodable do here? For me it seems that it is the same as passing value directly, if the value is nil - nothing will be encoded, non-nil - it will be encoded.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because these are overloading methods with same signature, so I wrap them in a struct so it will trigger func encodeIfPresent<T: Encodable>(_ value: T, forKey key: Key) instead, otherwise I would have to do the check for shouldEncodeNilValues in all the methods of encodeIfPresent
Another approach would be having a private method

private func encodeNilIfNeeded<T: Encodable>(_ value: T?, forKey key: Key) throws {
  guard value != nil || shouldEncodeNilValues else { return }
  try encode(value, forKey: key)
}

then have all the encodeIfPresent to point to this one, what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, makes sense 👍 I'm not sure if that is the case of premature optimization, but it seems to me that wrapping every primitive into a whole struct is could impact encoding performance. Because of this I'd prefer to have it as a separate method(like encodeNilIfNeeded in your comment).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is done now :)

Copy link
Contributor

@evil159 evil159 left a comment

Choose a reason for hiding this comment

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

Well done 👍

@maios maios merged commit eeccb0e into main Aug 3, 2022
@maios maios deleted the feature/MAPSIOS-118_layer-encode-nil branch August 3, 2022 05:08
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