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

Strip product metadata #2756

Merged
merged 8 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.wordpress.android.fluxc.model

import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import org.wordpress.android.fluxc.model.WCProductModel.AddOnsMetadataKeys
import org.wordpress.android.fluxc.model.WCProductModel.QuantityRulesMetadataKeys
import org.wordpress.android.fluxc.utils.EMPTY_JSON_ARRAY
import org.wordpress.android.fluxc.utils.isElementNullOrEmpty
import javax.inject.Inject

class StripProductMetaData @Inject internal constructor(private val gson: Gson) {
operator fun invoke(metadata: String?): String {
if (metadata.isNullOrEmpty()) return EMPTY_JSON_ARRAY

return gson.fromJson(metadata, JsonArray::class.java)
.mapNotNull { it as? JsonObject }
.asSequence()
.filter { jsonObject ->
val isNullOrEmpty = jsonObject[WCMetaData.VALUE].isElementNullOrEmpty()
jsonObject[WCMetaData.KEY]?.asString.orEmpty() in SUPPORTED_KEYS && isNullOrEmpty.not()
}.toList()
.takeIf { it.isNotEmpty() }
?.let { gson.toJson(it) } ?: EMPTY_JSON_ARRAY
}
Comment on lines +13 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

Just out of curiosity, I remember reviewing a code like this in the past. Did we implement a metadata strip before that got removed? I'm not finding anything like this in the trunk code base 😅

Copy link
Contributor Author

@atorresveiga atorresveiga Jun 21, 2023

Choose a reason for hiding this comment

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

I remember reviewing a code like this in the past.

Yes, but it was for product variations, not products. This is the strip use case class StripProductVariationMetaData


companion object {
val SUPPORTED_KEYS: Set<String> = buildSet {
add(AddOnsMetadataKeys.ADDONS_METADATA_KEY)
addAll(QuantityRulesMetadataKeys.ALL_KEYS)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.yarolegovich.wellsql.core.annotation.Column
import com.yarolegovich.wellsql.core.annotation.PrimaryKey
import com.yarolegovich.wellsql.core.annotation.Table
import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId
import org.wordpress.android.fluxc.model.WCProductModel.AddOnsMetadataKeys.ADDONS_METADATA_KEY
import org.wordpress.android.fluxc.model.WCProductVariationModel.ProductVariantOption
import org.wordpress.android.fluxc.model.addons.RemoteAddonDto
import org.wordpress.android.fluxc.network.utils.getBoolean
Expand All @@ -26,10 +27,6 @@ import org.wordpress.android.util.AppLog.T
*/
@Table(addOn = WellSqlConfig.ADDON_WOOCOMMERCE)
data class WCProductModel(@PrimaryKey @Column private var id: Int = 0) : Identifiable {
companion object {
private const val ADDONS_METADATA_KEY = "_product_addons"
}

@Column var localSiteId = 0
@Column var remoteProductId = 0L // The unique identifier for this product on the server
val remoteId
Expand Down Expand Up @@ -557,6 +554,10 @@ data class WCProductModel(@PrimaryKey @Column private var id: Int = 0) : Identif
return storedFiles == updatedFiles
}

object AddOnsMetadataKeys {
const val ADDONS_METADATA_KEY = "_product_addons"
}

object QuantityRulesMetadataKeys {
const val MINIMUM_ALLOWED_QUANTITY = "minimum_allowed_quantity"
const val MAXIMUM_ALLOWED_QUANTITY = "maximum_allowed_quantity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.wordpress.android.fluxc.generated.endpoint.WPAPI
import org.wordpress.android.fluxc.generated.endpoint.WPCOMREST
import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.StripProductMetaData
import org.wordpress.android.fluxc.model.StripProductVariationMetaData
import org.wordpress.android.fluxc.model.WCProductCategoryModel
import org.wordpress.android.fluxc.model.WCProductImageModel
Expand Down Expand Up @@ -82,6 +83,7 @@ class ProductRestClient @Inject constructor(
private val wooNetwork: WooNetwork,
private val wpComNetwork: WPComNetwork,
private val coroutineEngine: CoroutineEngine,
private val stripProductMetaData: StripProductMetaData,
private val stripProductVariationMetaData: StripProductVariationMetaData
) {
/**
Expand Down Expand Up @@ -291,6 +293,7 @@ class ProductRestClient @Inject constructor(
response.data?.let {
val newModel = it.asProductModel().apply {
localSiteId = site.id
metadata = stripProductMetaData(metadata)
}
RemoteProductPayload(newModel, site)
} ?: RemoteProductPayload(
Expand Down Expand Up @@ -410,7 +413,10 @@ class ProductRestClient @Inject constructor(
when (response) {
is WPAPIResponse.Success -> {
val productModels = response.data?.map {
it.asProductModel().apply { localSiteId = site.id }
it.asProductModel().apply {
localSiteId = site.id
metadata = stripProductMetaData(metadata)
}
}.orEmpty()

val loadedMore = offset > 0
Expand Down Expand Up @@ -520,7 +526,10 @@ class ProductRestClient @Inject constructor(
return response.toWooPayload { products ->
products.map {
it.asProductModel()
.apply { localSiteId = site.id }
.apply {
localSiteId = site.id
metadata = stripProductMetaData(metadata)
}
}
}
}
Expand Down Expand Up @@ -876,6 +885,7 @@ class ProductRestClient @Inject constructor(
response.data?.let {
val newModel = it.asProductModel().apply {
localSiteId = site.id
metadata = stripProductMetaData(metadata)
}
val payload = RemoteUpdateProductPayload(site, newModel)
dispatcher.dispatch(WCProductActionBuilder.newUpdatedProductAction(payload))
Expand Down Expand Up @@ -1189,6 +1199,7 @@ class ProductRestClient @Inject constructor(
response.data?.let {
val newModel = it.asProductModel().apply {
localSiteId = site.id
metadata = stripProductMetaData(metadata)
}
val payload = RemoteUpdateProductImagesPayload(site, newModel)
dispatcher.dispatch(WCProductActionBuilder.newUpdatedProductImagesAction(payload))
Expand Down Expand Up @@ -1486,6 +1497,7 @@ class ProductRestClient @Inject constructor(
val newModel = product.asProductModel().apply {
id = product.id?.toInt() ?: 0
localSiteId = site.id
metadata = stripProductMetaData(metadata)
Copy link
Contributor

@ThomazFB ThomazFB Jun 20, 2023

Choose a reason for hiding this comment

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

I I was wondering if maybe the stripProductMetaData use case could be called inside the asProductModel() transformation since all data strip happens right after it for every scenario. It could also be easier to maintain and enforce the metadata removal for future implementations.

But I also understand that since the stripping happens through a use case, the dependency injection will get weird and we would have to create the use case object manually, so if you think it's not worth the effort, it's OK too. I'm only concerned with making sure that we won't easily forget to strip that metadata in future changes.

WDYT?

Copy link
Contributor Author

@atorresveiga atorresveiga Jun 21, 2023

Choose a reason for hiding this comment

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

I'm only concerned with making sure that we won't easily forget to strip that metadata in future changes.

Good point @ThomazFB. ☝️

But I also understand that since the stripping happens through a use case, the dependency injection will get weird and we would have to create the use case object manually

That was also why I avoided adding the use case manually. I decided to go this way because I saw that we also had this line localSiteId = site.id on every call, adding the site id to the product model (so, we always pre-process the product before saving it to the database).
That said, I believe we can do better, so I'll add a task to refactor the asProductModel() and create a map class that can handle the injection of the use case. I will tackle this in a different PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! Thanks for that!

}
val payload = RemoteAddProductPayload(site, newModel)
dispatcher.dispatch(WCProductActionBuilder.newAddedProductAction(payload))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.wordpress.android.fluxc.utils

import com.google.gson.JsonElement

const val EMPTY_JSON_ARRAY = "[]"

fun JsonElement?.isElementNullOrEmpty(): Boolean {
return this?.let {
when{
this.isJsonObject -> this.asJsonObject.size() == 0
this.isJsonArray -> this.asJsonArray.size()== 0
this.isJsonPrimitive && this.asJsonPrimitive.isString -> this.asString.isEmpty()
this.isJsonNull -> true
else -> false
}
} ?: true
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ProductRestClientTest {
private val wpComNetwork: WPComNetwork = mock()

@Before fun setUp() {
sut = ProductRestClient(mock(), wooNetwork, wpComNetwork, initCoroutineEngine(), mock())
sut = ProductRestClient(mock(), wooNetwork, wpComNetwork, initCoroutineEngine(), mock(), mock())
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package org.wordpress.android.fluxc.network.rest.wpcom.wc.product

import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import org.assertj.core.api.Assertions
import org.junit.Test
import org.wordpress.android.fluxc.model.StripProductMetaData
import org.wordpress.android.fluxc.model.WCMetaData
import org.wordpress.android.fluxc.utils.EMPTY_JSON_ARRAY

class StripProductMetaDataTest {
private val gson = Gson()
private val sut = StripProductMetaData(gson)


@Test
fun `when metadata contains not supported keys, then NOT supported keys are stripped`() {
val result = sut.invoke(notOnlySupportedMetadata)
val jsonResult = gson.fromJson(result, JsonArray::class.java)

jsonResult.map { it as? JsonObject }.forEach { jsonObject ->
Assertions.assertThat(jsonObject).isNotNull
Assertions.assertThat(jsonObject?.get(WCMetaData.KEY)?.asString)
.isIn(StripProductMetaData.SUPPORTED_KEYS)
}
}

@Test
fun `when metadata contains a supported key with a NULL value, then strip the supported key`() {
val supportedKey = StripProductMetaData.SUPPORTED_KEYS.first()
val value: String? = null

val metadata = getOneItemMetadata(supportedKey, value)
val result = sut.invoke(metadata)

Assertions.assertThat(result).isEqualTo(EMPTY_JSON_ARRAY)
}

@Test
fun `when metadata contains a supported key with a EMPTY value, then strip the supported key`() {
val supportedKey = StripProductMetaData.SUPPORTED_KEYS.first()
val value = ""

val metadata = getOneItemMetadata(supportedKey, value)
val result = sut.invoke(metadata)

Assertions.assertThat(result).isEqualTo(EMPTY_JSON_ARRAY)
}

@Test
fun `when metadata contains a supported key with a NOT EMPTY value, then value is kept`() {
val supportedKey = StripProductMetaData.SUPPORTED_KEYS.first()
val value = "valid"

val metadata = getOneItemMetadata(supportedKey, value)
val result = sut.invoke(metadata)

val jsonResult = gson.fromJson(result, JsonArray::class.java)
val resultItem = jsonResult[0] as JsonObject
Assertions.assertThat(result).isNotNull
Assertions.assertThat(resultItem[WCMetaData.KEY].asString).isEqualTo(supportedKey)
Assertions.assertThat(resultItem[WCMetaData.VALUE].asString).isEqualTo(value)
}

@Test
fun `when metadata is null, then empty array is return`() {
val result = sut.invoke(null)
Assertions.assertThat(result).isEqualTo(EMPTY_JSON_ARRAY)
}

private fun getOneItemMetadata(itemKey: String, itemValue: String?): String {
val item = JsonObject().apply {
addProperty(WCMetaData.KEY, itemKey)
itemValue?.let {
addProperty(WCMetaData.VALUE, it)
} ?: add(WCMetaData.VALUE, null)
}
val jsonArray = JsonArray().apply {
add(item)
}

return gson.toJson(jsonArray)
}

private val notOnlySupportedMetadata = """
[
{
"id": 11749,
"key": "_wpcom_is_markdown",
"value": "1"
},
{
"id": 11750,
"key": "_last_editor_used_jetpack",
"value": "classic-editor"
},
{
"id": 11754,
"key": "_product_addons",
"value": [{
"id": 11773,
"key": "_wc_gla_sync_status",
"value": "synced"
},
{
"id": 11774,
"key": "group_of_quantity",
"value": ""
}]
},
{
"id": 11755,
"key": "_product_addons_exclude_global",
"value": "1"
},
{
"id": 11772,
"key": "_wc_gla_visibility",
"value": "sync-and-show"
},
{
"id": 11773,
"key": "_wc_gla_sync_status",
"value": "synced"
},
{
"id": 11774,
"key": "group_of_quantity",
"value": ""
},
{
"id": 11775,
"key": "minimum_allowed_quantity",
"value": ""
},
{
"id": 11776,
"key": "maximum_allowed_quantity",
"value": ""
},
{
"id": 11777,
"key": "minmax_do_not_count",
"value": "no"
},
{
"id": 11778,
"key": "minmax_cart_exclude",
"value": "no"
},
{
"id": 11779,
"key": "minmax_category_group_of_exclude",
"value": "no"
},
{
"id": 11780,
"key": "_wcsatt_disabled",
"value": "yes"
},
{
"id": 11781,
"key": "_subscription_one_time_shipping",
"value": "no"
},
{
"id": 11782,
"key": "_wcsatt_force_subscription",
"value": "no"
},
{
"id": 11785,
"key": "_subscription_downloads_ids",
"value": ""
},
{
"id": 11786,
"key": "minimum_allowed_quantity",
"value": "40"
},
{
"id": 11787,
"key": "_wc_pre_orders_fee",
"value": ""
},
{
"id": 11788,
"key": "_wpas_done_all",
"value": "1"
},
{
"id": 11909,
"key": "_wc_gla_mc_status",
"value": "not_synced"
},
{
"id": 12406,
"key": "_wc_gla_synced_at",
"value": "1686193267"
},
{
"id": 12407,
"key": "_wc_gla_google_ids",
"value": {
"US": "online:en:US:gla_418"
}
},
{
"key": "_satt_data",
"value": {
"subscription_schemes": []
}
}
]
""".trimIndent()
}