diff --git a/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/BannerConfig.kt b/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/BannerConfig.kt new file mode 100644 index 0000000..4170c1d --- /dev/null +++ b/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/BannerConfig.kt @@ -0,0 +1,84 @@ +package com.topsort.analytics.banners + +import com.topsort.analytics.model.auctions.Device + +/** + * Class that handles different type of Banner configurations + */ +sealed class BannerConfig private constructor() { + + /** + * Banner configuration for landing page banners + * + * @property slotId id of the banner slot + * @property ids ids of the entities that are competing for the banner + * @property device target device for the banner + * @property geoTargeting optional location for geo-targeted banners + */ + data class LandingPage( + val slotId: String, + val ids: List, + val device: Device = Device.mobile, + val geoTargeting: String? = null + ) : BannerConfig() + + /** + * Banner configuration for single category banners + * + * @property slotId id of the banner slot + * @property category category for the banner + * @property device target device for the banner + * @property geoTargeting optional location for geo-targeted banners + */ + data class CategorySingle( + val slotId: String, + val category: String, + val device: Device = Device.mobile, + val geoTargeting: String? = null + ) : BannerConfig() + + /** + * Banner config for multiple category banners + * + * @property slotId id of the banner slot + * @property categories list of categories for the competing banners + * @property device target device for the banner + * @property geoTargeting optional location for geo-targeted banners + */ + data class CategoryMultiple( + val slotId: String, + val categories: List, + val device: Device = Device.mobile, + val geoTargeting: String? = null, + ) : BannerConfig() + + /** + * Banner configuration for category disjunctions banners + * + * @property slotId id of the banner slot + * @property disjunctions category disjunctions for the competing banners + * @property device target device for the banner + * @property geoTargeting optional location for geo-targeted banners + */ + data class CategoryDisjunctions( + val slotId: String, + val disjunctions: List>, + val device: Device = Device.mobile, + val geoTargeting: String? = null, + ) : BannerConfig() + + /** + * Banner configuration for keyword banners + * + * @property slotId id of the banner slot + * @property keyword keyword for the competing banners + * @property device target device for the banner + * @property geoTargeting optional location for geo-targeted banners + */ + data class Keyword( + val slotId: String, + val keyword: String, + val device: Device = Device.mobile, + val geoTargeting: String? = null, + ) : BannerConfig() +} \ No newline at end of file diff --git a/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/BannerResponse.kt b/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/BannerResponse.kt new file mode 100644 index 0000000..4f75eb6 --- /dev/null +++ b/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/BannerResponse.kt @@ -0,0 +1,18 @@ +package com.topsort.analytics.banners + +import com.topsort.analytics.model.auctions.EntityType + +/** + * Response for a single slot banner auction + * + * @property id id of the winning entity + * @property type type of the winning entity + * @property url url of the banner to show + * @property resolvedBidId id for tracking the auction result on events + */ +data class BannerResponse( + val id: String, + val type: EntityType, + val url: String, + val resolvedBidId: String, +) \ No newline at end of file diff --git a/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/run.kt b/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/run.kt new file mode 100644 index 0000000..fa0fc06 --- /dev/null +++ b/TopsortAnalytics/src/main/java/com/topsort/analytics/banners/run.kt @@ -0,0 +1,87 @@ +package com.topsort.analytics.banners + +import com.topsort.analytics.model.auctions.Auction +import com.topsort.analytics.model.auctions.AuctionRequest +import com.topsort.analytics.service.TopsortAuctionsHttpService + +/** + * Run a banner auction with a single slot + * + * @param config the banner configuration that specifies which kind of banner auction to run + * @return A BannerResponse if the auction successfully returned a winner or null if not. + */ +fun runBannerAuction(config: BannerConfig): BannerResponse? { + val auction = buildBannerAuction(config) + val request = AuctionRequest(listOf(auction)) + val response = TopsortAuctionsHttpService.runAuctions(request) + if ((response?.results?.isNotEmpty() == true)) { + if (response.results[0].winners.isNotEmpty()) { + val winner = response.results[0].winners[0] + return BannerResponse( + id = winner.id, + url = winner.asset!!.url, + type = winner.type, + resolvedBidId = winner.resolvedBidId + ) + } + } + return null +} + +/** + * Builds a low-level Auction object to be run with TopsortAuctionHttpService. + * + * Generally, you shouldn't be calling this function yourself and you should use runBannerAuction instead. + * + * @param config the banner configuration that specifies which kind of banner auction to run + * @return an Auction object + */ +fun buildBannerAuction(config: BannerConfig): Auction { + when (config) { + is BannerConfig.LandingPage -> { + return Auction.Factory.buildBannerAuctionLandingPage( + 1, + config.slotId, + config.ids, + config.device, + config.geoTargeting + ) + } + + is BannerConfig.CategorySingle -> { + return Auction.Factory.buildBannerAuctionCategorySingle( + 1, + config.slotId, + config.category, + config.device, + config.geoTargeting + ) + } + + is BannerConfig.CategoryMultiple -> { + return Auction.Factory.buildBannerAuctionCategoryMultiple( + 1, + config.slotId, + config.categories, + config.device, + config.geoTargeting + ) + } + + is BannerConfig.CategoryDisjunctions -> { + return Auction.Factory.buildBannerAuctionCategoryDisjunctions( + 1, config.slotId, config.disjunctions, config.device, config.geoTargeting + ) + } + + is BannerConfig.Keyword -> { + return Auction.Factory.buildBannerAuctionKeywords( + 1, + config.slotId, + config.keyword, + config.device, + config.geoTargeting + ) + } + } +} \ No newline at end of file diff --git a/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/Auction.kt b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/Auction.kt index 59af3af..7babc7f 100644 --- a/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/Auction.kt +++ b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/Auction.kt @@ -1,6 +1,6 @@ package com.topsort.analytics.model.auctions -data class Auction private constructor ( +data class Auction private constructor( val type: String, val slots: Int, val products: Products? = null, @@ -8,17 +8,17 @@ data class Auction private constructor ( val searchQuery: String? = null, val geoTargeting: GeoTargeting? = null, val slotId: String? = null, - val device: String? = null, + val device: Device? = null, ) { - object Factory{ + object Factory { @JvmOverloads fun buildSponsoredListingAuctionProductIds( - slots : Int, + slots: Int, ids: List, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "listings", slots = slots, @@ -29,10 +29,10 @@ data class Auction private constructor ( @JvmOverloads fun buildSponsoredListingAuctionCategorySingle( - slots : Int, + slots: Int, category: String, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "listings", slots = slots, @@ -43,10 +43,10 @@ data class Auction private constructor ( @JvmOverloads fun buildSponsoredListingAuctionCategoryMultiple( - slots : Int, + slots: Int, categories: List, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "listings", slots = slots, @@ -57,10 +57,10 @@ data class Auction private constructor ( @JvmOverloads fun buildSponsoredListingAuctionCategoryDisjunctions( - slots : Int, + slots: Int, disjunctions: List>, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "listings", slots = slots, @@ -71,10 +71,10 @@ data class Auction private constructor ( @JvmOverloads fun buildSponsoredListingAuctionKeyword( - slots : Int, + slots: Int, keyword: String, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "listings", slots = slots, @@ -85,12 +85,12 @@ data class Auction private constructor ( @JvmOverloads fun buildBannerAuctionLandingPage( - slots : Int, - slotId : String, + slots: Int, + slotId: String, ids: List, - device: String? = null, + device: Device = Device.mobile, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "banners", slots = slots, @@ -103,12 +103,12 @@ data class Auction private constructor ( @JvmOverloads fun buildBannerAuctionCategorySingle( - slots : Int, - slotId : String, + slots: Int, + slotId: String, category: String, - device: String? = null, + device: Device = Device.mobile, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "banners", slots = slots, @@ -121,12 +121,12 @@ data class Auction private constructor ( @JvmOverloads fun buildBannerAuctionCategoryMultiple( - slots : Int, - slotId : String, + slots: Int, + slotId: String, categories: List, - device: String? = null, + device: Device = Device.mobile, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "banners", slots = slots, @@ -139,12 +139,12 @@ data class Auction private constructor ( @JvmOverloads fun buildBannerAuctionCategoryDisjunctions( - slots : Int, - slotId : String, + slots: Int, + slotId: String, disjunctions: List>, - device: String? = null, + device: Device = Device.mobile, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "banners", slots = slots, @@ -157,12 +157,12 @@ data class Auction private constructor ( @JvmOverloads fun buildBannerAuctionKeywords( - slots : Int, - slotId : String, + slots: Int, + slotId: String, keyword: String, - device: String? = null, + device: Device = Device.mobile, geoTargeting: String? = null, - ) : Auction { + ): Auction { return Auction( type = "banners", slots = slots, @@ -174,7 +174,7 @@ data class Auction private constructor ( } } - data class Products ( + data class Products( val ids: List, ) diff --git a/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/AuctionResponse.kt b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/AuctionResponse.kt index 3f34d01..cdc3c9e 100644 --- a/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/AuctionResponse.kt +++ b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/AuctionResponse.kt @@ -3,11 +3,11 @@ package com.topsort.analytics.model.auctions import org.json.JSONObject data class AuctionResponse private constructor( - val results : List? = null, + val results: List, ) { companion object { fun fromJson(json: String?): AuctionResponse? { - if(json == null) return null + if (json == null) return null val array = JSONObject(json).getJSONArray("results") val results = (0 until array.length()).map { AuctionResponseItem.fromJsonObject(array.getJSONObject(it)) @@ -20,11 +20,11 @@ data class AuctionResponse private constructor( } data class AuctionResponseItem( - val resultType : String, - val winners : List? = null, + val resultType: String, + val winners: List, val error: Boolean, ) { - companion object{ + companion object { fun fromJsonObject(json: JSONObject): AuctionResponseItem { val array = json.getJSONArray("winners") val winners = (0 until array.length()).map { @@ -40,20 +40,33 @@ data class AuctionResponse private constructor( } data class AuctionWinnerItem( - val rank : Int, - val type : String, - val id : String, + val rank: Int, + val type: EntityType, + val id: String, val resolvedBidId: String, - ){ - companion object{ + val asset: Asset? = null, + ) { + companion object { fun fromJsonObject(json: JSONObject): AuctionWinnerItem { return AuctionWinnerItem( - rank = json.getInt("rank"), - type = json.getString("type"), - id = json.getString("id"), - resolvedBidId = json.getString("resolvedBidId"), + rank = json.getInt("rank"), + type = EntityType.fromValue(json.getString("type")), + id = json.getString("id"), + resolvedBidId = json.getString("resolvedBidId"), + asset = Asset.fromJsonObject(json), ) } } } + + data class Asset(val url: String) { + companion object { + fun fromJsonObject(json: JSONObject): Asset? { + val asset = json.optJSONObject("asset") ?: return null + val url = asset.getString("url") + return Asset(url = url) + } + } + } } + diff --git a/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/Device.kt b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/Device.kt new file mode 100644 index 0000000..e80aec8 --- /dev/null +++ b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/Device.kt @@ -0,0 +1,7 @@ +package com.topsort.analytics.model.auctions + +@Suppress("EnumNaming") +enum class Device { + desktop, + mobile +} \ No newline at end of file diff --git a/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/EntityType.kt b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/EntityType.kt new file mode 100644 index 0000000..2161bfc --- /dev/null +++ b/TopsortAnalytics/src/main/java/com/topsort/analytics/model/auctions/EntityType.kt @@ -0,0 +1,18 @@ +package com.topsort.analytics.model.auctions + +enum class EntityType { + PRODUCT, + VENDOR, + BRAND, + URL; + + companion object { + fun fromValue(value: String): EntityType = when (value) { + "product" -> PRODUCT + "vendor" -> VENDOR + "brand" -> BRAND + "url" -> URL + else -> throw IllegalArgumentException("not valid entity type: $value") + } + } +} diff --git a/TopsortAnalytics/src/test/java/com/topsort/analytics/banners/RunTest.kt b/TopsortAnalytics/src/test/java/com/topsort/analytics/banners/RunTest.kt new file mode 100644 index 0000000..28b013e --- /dev/null +++ b/TopsortAnalytics/src/test/java/com/topsort/analytics/banners/RunTest.kt @@ -0,0 +1,56 @@ +package com.topsort.analytics.banners + +import org.assertj.core.api.Assertions.assertThat +import org.json.JSONObject +import org.junit.Test + +internal class RunTest { + @Test + fun buildLandingPageBanner() { + val slot = "slot" + val ids = listOf("id1", "id2") + val bannerConfig = BannerConfig.LandingPage(slotId = slot, ids = ids) + val bannerAuction = buildBannerAuction(bannerConfig); + val json = JSONObject.wrap(bannerAuction)!!.toString() + val expectedJson = + "{\"slots\":1,\"slotId\":\"$slot\",\"type\":\"banners\",\"device\":\"mobile\",\"products\":{\"ids\":[\"${ids[0]}\",\"${ids[1]}\"]}}" + assertThat(json).isEqualTo(expectedJson) + } + + @Test + fun buildSingleCategoryBanner() { + val slot = "slot" + val category = "category" + val bannerConfig = BannerConfig.CategorySingle(slotId = slot, category = category) + val bannerAuction = buildBannerAuction(bannerConfig); + val json = JSONObject.wrap(bannerAuction)!!.toString() + val expectedJson = + "{\"slots\":1,\"slotId\":\"$slot\",\"type\":\"banners\",\"category\":{\"id\":\"$category\"},\"device\":\"mobile\"}" + assertThat(json).isEqualTo(expectedJson) + } + + @Test + fun buildMultipleCategoryBanner() { + val slot = "slot" + val categories = listOf("cat1", "cat2") + val bannerConfig = BannerConfig.CategoryMultiple(slotId = slot, categories = categories) + val bannerAuction = buildBannerAuction(bannerConfig); + val json = JSONObject.wrap(bannerAuction)!!.toString() + val expectedJson = + "{\"slots\":1,\"slotId\":\"$slot\",\"type\":\"banners\",\"category\":{\"ids\":[\"${categories[0]}\",\"${categories[1]}\"]},\"device\":\"mobile\"}" + assertThat(json).isEqualTo(expectedJson) + } + + @Test + fun buildDisjuctionsCategoryBanner() { + val slot = "slot" + val disjunctions = listOf(listOf("cat1", "cat2"), listOf("cat3")) + val bannerConfig = + BannerConfig.CategoryDisjunctions(slotId = slot, disjunctions = disjunctions) + val bannerAuction = buildBannerAuction(bannerConfig); + val json = JSONObject.wrap(bannerAuction)!!.toString() + val expectedJson = + "{\"slots\":1,\"slotId\":\"$slot\",\"type\":\"banners\",\"category\":{\"disjunctions\":[[\"${disjunctions[0][0]}\",\"${disjunctions[0][1]}\"],[\"${disjunctions[1][0]}\"]]},\"device\":\"mobile\"}" + assertThat(json).isEqualTo(expectedJson) + } +} \ No newline at end of file