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

Offering metadata #2498

Merged
merged 21 commits into from
May 24, 2023
Merged

Offering metadata #2498

merged 21 commits into from
May 24, 2023

Conversation

joshdholtz
Copy link
Member

@joshdholtz joshdholtz commented May 17, 2023

Motivation

Added metadata to Offering 😊

Description

  • Added metadata to Offering
    • Had to use a private internal Metadata struct to make Swift compiler happy
  • Updated unit tests
  • Updated backend integration tests
  • Updated load shedder tests

NachoSoto added a commit that referenced this pull request May 17, 2023
### Changes:
- Added `SnapshotTesting` to `BackendIntegrationTests` target
- Verify `OfferingsResponse` content with snapshots

This relies on the new serializable `OfferingsResponse` from #2495. It will make it trivial to verify the changes with the upcoming metadata in #2498.
NachoSoto added a commit that referenced this pull request May 17, 2023
### Changes:
- Added `SnapshotTesting` to `BackendIntegrationTests` target
- Verify `OfferingsResponse` content with snapshots

This relies on the new serializable `OfferingsResponse` from #2495. It will make it trivial to verify the changes with the upcoming metadata in #2498.
NachoSoto added a commit that referenced this pull request May 18, 2023
### Changes:
- Added `SnapshotTesting` to `BackendIntegrationTests` target
- Verify `OfferingsResponse` content with snapshots

This relies on the new serializable `OfferingsResponse` from #2495. It
will make it trivial to verify the changes with the upcoming metadata in
#2498.
@joshdholtz joshdholtz added the pr:feat A new feature label May 19, 2023
@codecov
Copy link

codecov bot commented May 20, 2023

Codecov Report

Merging #2498 (ac7f805) into main (d5b9236) will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           Coverage Diff           @@
##             main    #2498   +/-   ##
=======================================
  Coverage   87.85%   87.85%           
=======================================
  Files         199      199           
  Lines       13655    13664    +9     
=======================================
+ Hits        11996    12005    +9     
  Misses       1659     1659           
Impacted Files Coverage Δ
...urces/Networking/Responses/OfferingsResponse.swift 100.00% <ø> (ø)
Sources/Purchasing/Offering.swift 93.33% <100.00%> (+0.65%) ⬆️
Sources/Purchasing/OfferingsFactory.swift 100.00% <100.00%> (ø)

... and 2 files with indirect coverage changes

Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

I think this is ready 👍🏻 Just need to fix those potential crashes in the test, I'll send a PR on Monday with a new method on Collection so we can do that.

@@ -4,6 +4,9 @@
{
"description" : "standard set of packages",
"identifier" : "default",
"metadata" : {
Copy link
Contributor

Choose a reason for hiding this comment

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

Amazing

@@ -48,10 +49,48 @@ class OfferingsDecodingTests: BaseHTTPResponseTest {
}

func testDecodesSecondOffering() throws {
let offering = try XCTUnwrap(self.response.offerings.last)
let offering = try XCTUnwrap(self.response.offerings[1])
Copy link
Contributor

Choose a reason for hiding this comment

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

This will make the test crash. I'll help you on Monday, we'll need to define a new method that returns an Optional for an index.

}

func testDecodesMetadataOffering() throws {
let offering = try XCTUnwrap(self.response.offerings[2])
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

expect(offering.metadata) == [:]
expect(offering.packages).to(haveCount(1))

let package = offering.packages[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid the test crashing (instead of failing):

Suggested change
let package = offering.packages[0]
let package = try XCTUnwrap(offering.packages.onlyElement)

]
expect(offering.packages).to(haveCount(1))

let package = offering.packages[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto:

Suggested change
let package = offering.packages[0]
try XCTUnwrap(offering.packages.onlyElement)

}

func testDecodesNullMetadataOffering() throws {
let offering = try XCTUnwrap(self.response.offerings[3])
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

@joshdholtz joshdholtz requested a review from NachoSoto May 22, 2023 02:11
@joshdholtz
Copy link
Member Author

@NachoSoto I think I fixed all the safety things that could crash the tests! But let me know if you want me to do it a different way or move code around 😇

Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

This is ready :) just one more suggested test.

private extension Collection {

/// Returns the element at the specified index if it exists, otherwise nil.
subscript (safe index: Index) -> Element? {
Copy link
Contributor

Choose a reason for hiding this comment

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

Cool this is exactly what I had in mind 👍🏻
I can extract this an add unit tests for it in a separate PR since I think it's useful to have.

expect(offering.metadata) == [:]
expect(offering.packages).to(haveCount(1))

let package = try XCTUnwrap(offering.packages[safe: 0])
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd use first here which is the same:

Suggested change
let package = try XCTUnwrap(offering.packages[safe: 0])
let package = try XCTUnwrap(offering.packages.first)

@@ -495,6 +495,7 @@ private extension OfferingsManagerTests {
Offering(
identifier: offering.identifier,
serverDescription: offering.description,
metadata: [:],
Copy link
Contributor

Choose a reason for hiding this comment

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

The other tests only check OfferingsResponse. Let's add some metadata here so you can check in another test in this file that they get copied correctly to Offering (basically thatoffering.metadata.mapValues(\.asAny)).

@joshdholtz joshdholtz requested a review from NachoSoto May 22, 2023 20:25
@joshdholtz joshdholtz changed the title [WIP] Offering metadata Offering metadata May 22, 2023
static let anyBackendOfferingsResponse: OfferingsResponse = .init(
currentOfferingId: "base",
offerings: [
.init(identifier: "base",
description: "This is the base offering",
packages: [
.init(identifier: "$rc_monthly", platformProductIdentifier: "monthly_freetrial")
])
],
metadata: _metadata)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit per our style guide:

Suggested change
metadata: _metadata)
metadata: Self.metadata)

@@ -465,14 +466,27 @@ private extension OfferingsManagerTests {
enum MockData {
static let anyAppUserID = ""

@DefaultDecodable.EmptyDictionary
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm this shouldn't be here since this type isn't Decodable.

@@ -153,6 +153,55 @@ class OfferingsTests: TestCase {
expect(offerings.current) == offerings["offering_a"]
}

func testOfferingsWithMetadataIsCreated() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻

extension Offering {

struct Metadata {
let data: [String: Any]
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder, if we want to force developers to always have a default value, should we hide the raw data and provide a method to access values from the metadata that requires users to put a default value? Something like func getValue(key: String, defaultValue: Any)

Copy link
Contributor

@NachoSoto NachoSoto May 23, 2023

Choose a reason for hiding this comment

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

Yeah, or maybe as an additional API 👍🏻

extension Offering {
  func getMetadataValue<T>(for key: String, defaultValue: T) -> T {
    return self.metadata.data[key] as? T ?? defaultValue
  }
} 

This would be Swift only, but I think it would be useful.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, we might want to have different accessors so people don't need to handle Any. Things like getStringValue, getIntValue, getBoolValue,... Not sure if we want to support different types though!

Copy link
Contributor

Choose a reason for hiding this comment

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

That's not very Swifty.
For Obj-C the current API works well.
For Swift I'd go with my suggested generic API.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we want getMetadataValue() on Offering? Or should we make the Metadata object public with a subscript thing?

Something like this on Metadata... 🤷‍♂️

public subscript<T>(key: String, defaultValue defaultValue: T) -> T {
    return self.data[key] as? T ?? defaultValue
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Metadata is a struct therefore it wouldn't be available to ObjC, so it needs to be on Offering.

Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

Marking this as request changes since I want to look at the final version with the pending changes :)

Sources/Purchasing/Offering.swift Show resolved Hide resolved
@joshdholtz joshdholtz marked this pull request as ready for review May 23, 2023 19:00
@joshdholtz joshdholtz requested a review from NachoSoto May 23, 2023 19:13
@NachoSoto NachoSoto changed the title Offering metadata Offering metadata May 23, 2023
Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

Almost ready! Just some nit-picks about the API.

extension Offering {

/**
Gets a value from `metadata` and falls back to a specified default value.
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe:

Suggested change
Gets a value from `metadata` and falls back to a specified default value.
- Returns: the `metadata` value associated to `key` for the expected type,
or `defaultValue` if not found, or it's not the expected type.

/**
Gets a value from `metadata` and falls back to a specified default value.
*/
public func getMetadataValue<T>(for key: String, defaultValue: T) -> T {
Copy link
Contributor

Choose a reason for hiding this comment

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

Being nit-picky since this is public so we want to get it right from the beginning.
Maybe:

Suggested change
public func getMetadataValue<T>(for key: String, defaultValue: T) -> T {
public func getMetadataValue<T>(for key: String, default: T) -> T {

Swift guidelines require avoiding that alliteration.

Comment on lines 33 to 34
let metadataString: String = off.getMetadataValue(for: "", defaultValue: "")
let metadataInt: Int = off.getMetadataValue(for: "", defaultValue: 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

I know you were following the pattern here, but it's leftover from the first implementation which was kind of messy.
We should get rid of all these print calls.

To avoid increasing the noise, I'd change these to:

Suggested change
let metadataString: String = off.getMetadataValue(for: "", defaultValue: "")
let metadataInt: Int = off.getMetadataValue(for: "", defaultValue: 0)
let _: String = off.getMetadataValue(for: "", defaultValue: "")
let _: Int = off.getMetadataValue(for: "", defaultValue: 0)

Comment on lines 201 to 202
let offeringA = try XCTUnwrap(offerings["offering_a"])
expect(offeringA.metadata.count) == metadata.count
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I'd change this to haveCount, otherwise the test is great 👍🏻

expect(offeringA.getMetadataValue(for: "int", defaultValue: 0)) == 5
expect(offeringA.getMetadataValue(for: "double", defaultValue: 0.0)) == 5.5
expect(offeringA.getMetadataValue(for: "boolean", defaultValue: false)) == true
expect(offeringA.getMetadataValue(for: "string", defaultValue: "")) == "five"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add tests for not finding the key, and also having the wrong type?

Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

Almost ready! Just some nit-picks about the API.

@@ -153,6 +153,59 @@ class OfferingsTests: TestCase {
expect(offerings.current) == offerings["offering_a"]
}

func testOfferingsWithMetadataIsCreated() throws {
@DefaultDecodable.EmptyDictionary
Copy link
Contributor

Choose a reason for hiding this comment

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

Also reminder to remove this

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is needed though 🤔 I couldn't get it to compile without that 😬

@joshdholtz joshdholtz requested a review from NachoSoto May 23, 2023 21:27
Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

Ship it!

Copy link
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

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

Looks good!

- Returns: the `metadata` value associated to `key` for the expected type,
or `defaultValue` if not found, or it's not the expected type.
*/
public func getMetadataValue<T>(for key: String, default: T) -> T {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this also support null default values? Like let metadataInt: Int? = offering.getMetadataValue(for: "key", default: nil). I think that could be useful.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should be let me write another test for that! 😊

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, this works!

let optionalInt: Int? = offeringA.getMetadataValue(for: "optionalInt", default: nil)
expect(optionalInt).to(beNil())

@joshdholtz joshdholtz merged commit 7fe13aa into main May 24, 2023
@joshdholtz joshdholtz deleted the offering-metadata branch May 24, 2023 17:06
NachoSoto added a commit that referenced this pull request May 25, 2023
### Changes:
- Added `SnapshotTesting` to `BackendIntegrationTests` target
- Verify `OfferingsResponse` content with snapshots

This relies on the new serializable `OfferingsResponse` from #2495. It
will make it trivial to verify the changes with the upcoming metadata in
#2498.
NachoSoto pushed a commit that referenced this pull request May 25, 2023
Added `metadata` to `Offering` 😊

- Added `metadata` to `Offering`
- Had to use a private internal `Metadata` struct to make Swift compiler
happy
- Updated unit tests
- Updated backend integration tests
- Updated load shedder tests
This was referenced May 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr:feat A new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants