Skip to content
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
2 changes: 2 additions & 0 deletions library/src/main/java/io/constructor/core/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Constants {
const val TERM_UNKNOWN = "TERM_UNKNOWN"
const val PAGE = "page"
const val PER_PAGE = "num_results_per_page"
const val SORT_BY = "sort_by"
const val SORT_ORDER = "sort_order"
const val FILTER_GROUP_ID = "filters[group_id]"
const val FILTER_FACET = "filters[%s]"
const val RESULT_ID = "result_id"
Expand Down
13 changes: 10 additions & 3 deletions library/src/main/java/io/constructor/core/ConstructorIo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ object ConstructorIo {
preferenceHelper.getSessionId(sessionIncrementHandler)
}

/**
* Returns a list of autocomplete suggestions
*/
fun getAutocompleteResults(query: String): Observable<ConstructorData<List<Suggestion>?>> {
val params = mutableListOf<Pair<String, String>>()
configMemoryHolder.autocompleteResultCount?.entries?.forEach {
Expand All @@ -93,13 +96,17 @@ object ConstructorIo {
return dataManager.getAutocompleteResults(query, params.toTypedArray())
}

fun getSearchResults(text: String, vararg facets: Pair<String, List<String>>, page: Int? = null, perPage: Int? = null, groupId: Int? = null): Observable<ConstructorData<SearchResponse>> {
preferenceHelper.getSessionId(sessionIncrementHandler)
/**
* Returns search results including filters, categories, sort options, etc.
*/
fun getSearchResults(text: String, facets: List<Pair<String, List<String>>>? = null, page: Int? = null, perPage: Int? = null, groupId: Int? = null, sortBy: String? = null, sortOrder: String? = null): Observable<ConstructorData<SearchResponse>> {
val encodedParams: ArrayList<Pair<String, String>> = arrayListOf()
groupId?.let { encodedParams.add(Constants.QueryConstants.FILTER_GROUP_ID.urlEncode() to it.toString()) }
page?.let { encodedParams.add(Constants.QueryConstants.PAGE.urlEncode() to page.toString().urlEncode()) }
perPage?.let { encodedParams.add(Constants.QueryConstants.PER_PAGE.urlEncode() to perPage.toString().urlEncode()) }
facets.forEach { facet ->
sortBy?.let { encodedParams.add(Constants.QueryConstants.SORT_BY.urlEncode() to it.urlEncode()) }
sortOrder?.let { encodedParams.add(Constants.QueryConstants.SORT_ORDER.urlEncode() to it.urlEncode()) }
facets?.forEach { facet ->
facet.second.forEach {
encodedParams.add(Constants.QueryConstants.FILTER_FACET.format(facet.first).urlEncode() to it.urlEncode())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.constructor.data.model.dataadapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import io.constructor.data.model.Group
import io.constructor.data.model.search.ResultData
import io.constructor.data.model.search.ResultFacet

class ResultDataAdapter {

companion object {
val NAMES = JsonReader.Options.of("id", "description", "image_url", "url", "facets", "groups")
}

@FromJson fun fromJson(jsonReader: JsonReader, facetDelegate: JsonAdapter<List<ResultFacet>>, groupDelegate: JsonAdapter<List<Group>>): ResultData {
jsonReader.beginObject()
var metadata: HashMap<String, Any?> = hashMapOf()
var id = ""
var description: String? = null
var imageUrl: String? = null
var url: String? = null
var facets: List<ResultFacet>? = null
var groups: List<Group>? = null
while (jsonReader.hasNext()) {
when (jsonReader.selectName(NAMES)) {
0 -> {
id = jsonReader.nextString()
}
1 -> {
description = jsonReader.nextString()
}
2 -> {
imageUrl = jsonReader.nextString()
}
3 -> {
url = jsonReader.nextString()

}
4 -> {
facets = facetDelegate.fromJsonValue(jsonReader.readJsonValue())
}
5 -> {
groups = groupDelegate.fromJsonValue(jsonReader.readJsonValue())
}
else -> {
metadata[jsonReader.nextName()] = jsonReader.readJsonValue()
}
}
}
jsonReader.endObject()
return ResultData(description, id, imageUrl, url, facets, groups, metadata)

}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package io.constructor.data.model.search

data class FacetOption(val count: Int, val value: String?)
import java.io.Serializable

data class FacetOption(val count: Int, val value: String?) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package io.constructor.data.model.search

import com.squareup.moshi.Json
import io.constructor.data.model.Group
import java.io.Serializable

data class ResultData(val description: String?,
val id: String,
@Json(name = "image_url") val imageUrl: String?,
val url: String?,
val facets: List<ResultFacet>?,
val groups: List<Group>?,
var metadata: Map<String, Any>?)
var metadata: Map<String, Any?>) : Serializable
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package io.constructor.data.model.search

data class ResultFacet(val name: String, val values: List<FacetOption>?)
import java.io.Serializable

data class ResultFacet(val name: String, val values: List<String>?) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package io.constructor.data.model.search
import com.squareup.moshi.Json


data class SearchData(val facets: List<SearchFacet>?, val groups: List<SearchGroup>?, val results: List<Result>?, @Json(name = "total_num_results") val resultCount: Int)
data class SearchData(val facets: List<SearchFacet>?, val groups: List<SearchGroup>?, @Json(name = "results") val searchResults: List<SearchResult>?, @Json(name = "sort_options") val sortOptions: List<SortOption>? = null, @Json(name = "total_num_results") val resultCount: Int)
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.constructor.data.model.search

import com.squareup.moshi.Json
import java.io.Serializable

data class SearchFacet(val name: String,
@Json(name = "display_name") val displayName: String?,
val status: Map<String, Any>?,
val type: String?,
val min: Int?,
val max: Int?,
val options: List<FacetOption>?)
val min: Double?,
val max: Double?,
val options: List<FacetOption>?) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import com.squareup.moshi.Json

data class SearchGroup(@Json(name = "children") val children: List<SearchGroup>?,
@Json(name = "parents") val parents: List<SearchGroup>?,
val count: Int,
val count: Int?,
@Json(name = "display_name") val displayName: String,
@Json(name = "group_id") val groupId: Long)
@Json(name = "group_id") val groupId: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.constructor.data.model.search

import com.squareup.moshi.Json
import java.io.Serializable

data class SearchResult(@Json(name = "data") val result: ResultData, @Json(name = "matched_terms") val matchedTerms: List<String>?, val value: String) : Serializable
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.constructor.data.model.search

import com.squareup.moshi.Json
import java.io.Serializable


data class SortOption(@Json(name = "display_name") val displayName: String,
@Json(name = "sort_by") val sortBy: String,
@Json(name = "sort_order") val sortOrder: String,
val status: String) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,40 @@ import retrofit2.http.*
interface ConstructorApi {

@GET(ApiPaths.URL_AUTOCOMPLETE)
fun getAutocompleteResults(@Path("value") value: String, @QueryMap data: Map<String, String>): Single<Result<AutocompleteResult>>
fun getAutocompleteResults(@Path("value") value: String,
@QueryMap data: Map<String, String>): Single<Result<AutocompleteResult>>

@GET(ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT)
fun trackAutocompleteSelect(@Path("term") term: String, @QueryMap data: Map<String, String>, @QueryMap(encoded = true) encodedData: Map<String, String>): Completable
fun trackAutocompleteSelect(@Path("term") term: String,
@QueryMap data: Map<String, String>,
@QueryMap(encoded = true) encodedData: Map<String, String>): Completable

@GET(ApiPaths.URL_SEARCH_SUBMIT_EVENT)
fun trackSearchSubmit(@Path("term") term: String, @QueryMap data: Map<String, String>, @QueryMap(encoded = true) encodedData: Map<String, String>): Completable
fun trackSearchSubmit(@Path("term") term: String,
@QueryMap data: Map<String, String>,
@QueryMap(encoded = true) encodedData: Map<String, String>): Completable

@GET(ApiPaths.URL_SESSION_START_EVENT)
fun trackSessionStart(@QueryMap params: Map<String, String>): Completable

@GET(ApiPaths.URL_CONVERSION_EVENT)
fun trackConversion(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @Query("revenue") revenue: String?, @QueryMap params: Map<String, String>): Completable
fun trackConversion(@Path("term") term: String,
@Query("name") itemName: String,
@Query("customer_id") customerId: String,
@Query("revenue") revenue: String?,
@QueryMap params: Map<String, String>): Completable

@GET(ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT)
fun trackSearchResultClick(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @QueryMap params: Map<String, String>, @QueryMap(encoded = true) encodedData: Map<String, String>): Completable
fun trackSearchResultClick(@Path("term") term: String,
@Query("name") itemName: String,
@Query("customer_id") customerId: String,
@QueryMap params: Map<String, String>,
@QueryMap(encoded = true) encodedData: Map<String, String>): Completable

@GET(ApiPaths.URL_BEHAVIOR)
fun trackSearchResultsLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map<String, String>): Completable
fun trackSearchResultsLoaded(@Query("term") term: String,
@Query("num_results") resultCount: Int,
@QueryMap params: Map<String, String>): Completable

@GET(ApiPaths.URL_BEHAVIOR)
fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map<String, String>): Completable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.constructor.BuildConfig
import io.constructor.data.interceptor.RequestInterceptor
import io.constructor.data.local.PreferencesHelper
import io.constructor.data.memory.ConfigMemoryHolder
import io.constructor.data.model.dataadapter.ResultDataAdapter
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
Expand Down Expand Up @@ -56,6 +57,7 @@ class NetworkModule(private val context: Context) {
@Singleton
internal fun provideMoshi(): Moshi = Moshi
.Builder()
.add(ResultDataAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,6 @@ class ConstructorIoAutocompleteTest {
assert(request.path.startsWith(path))
}

@Test
fun getAutocompleteResultsWithUnexpectedResponse() {
val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_with_unexpected_data.json"))
mockServer.enqueue(mockResponse)
val observer = constructorIo.getAutocompleteResults("titanic").test()
observer.assertComplete().assertValue {
it.get()!!.isNotEmpty() && it.get()!!.size == 5
}
val request = mockServer.takeRequest()
val path = "/autocomplete/titanic?key=golden-key&i=guido-the-guid&ui=player-one&s=79&c=cioand-1.3.0&_dt="
assert(request.path.startsWith(path))
}

@Test
fun getAutocompleteResultsWithEmptyResponse() {
val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_empty.json"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ class ConstructorIoSearchTest {
mockServer.enqueue(mockResponse)
val observer = constructorIo.getSearchResults("corn").test()
observer.assertComplete().assertValue {
it.get()!!.searchData.results!!.size == 20
it.get()!!.searchData.searchResults!!.size == 24
it.get()!!.searchData.searchResults!![0].value == "Del Monte Fresh Cut Corn Whole Kernel Golden Sweet with Natural Sea Salt - 15.25 Oz"
it.get()!!.searchData.searchResults!![0].result.id == "121150086"
it.get()!!.searchData.searchResults!![0].result.imageUrl == "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150086.png"
it.get()!!.searchData.searchResults!![0].result.metadata["price"] == 2.29
it.get()!!.searchData.searchResults!![0].matchedTerms!![0] == "corn"
it.get()!!.searchData.facets!!.size == 3
it.get()!!.searchData.facets!![0].displayName == "Brand"
it.get()!!.searchData.facets!![0].type == "multiple"
it.get()!!.searchData.groups!!.size == 1
it.get()!!.searchData.resultCount == 225
}
val request = mockServer.takeRequest()
val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt="
Expand Down Expand Up @@ -88,26 +98,16 @@ class ConstructorIoSearchTest {
assert(request.path.startsWith(path))
}

@Test
fun getSearchResultsWithUnexpectedResponse() {
val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_unexpected_data.json"))
mockServer.enqueue(mockResponse)
val observer = constructorIo.getSearchResults("corn").test()
observer.assertComplete().assertValue {
it.get()!!.searchData.resultCount == 23
}
val request = mockServer.takeRequest()
val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt="
assert(request.path.startsWith(path))
}

@Test
fun getSearchResultsWithEmptyResponse() {
val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_empty.json"))
mockServer.enqueue(mockResponse)
val observer = constructorIo.getSearchResults("corn").test()
observer.assertComplete().assertValue {
it.get()!!.searchData.results!!.isEmpty()
it.get()!!.searchData.searchResults!!.isEmpty()
it.get()!!.searchData.facets!!.isEmpty()
it.get()!!.searchData.groups!!.isEmpty()
it.get()!!.searchData.resultCount == 0
}
val request = mockServer.takeRequest()
val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt="
Expand Down
2 changes: 0 additions & 2 deletions library/src/test/java/io/constructor/util/TestDataLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ object TestDataLoader {

fun loadResponse() : AutocompleteResult? = loadResult("autocomplete_response.json")

fun loadResponseWithUnexpectedData() : AutocompleteResult? = loadResult("autocomplete_response_with_unexpected_data.json")

fun loadEmptyResponse() : AutocompleteResult? = loadResult("autocomplete_response_empty.json")

private fun loadResult(fileName: String): AutocompleteResult? {
Expand Down
Loading