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

feat/issue 128 #148

Merged
merged 13 commits into from
Jul 19, 2024
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Threshr is available to jvm projects via [maven central].
<dependency>
<groupId>com.graqr</groupId>
<artifactId>threshr</artifactId>
<version>0.0.13</version>
<version>0.0.14-SNAPSHOT</version>
</dependency>
```

Expand All @@ -64,14 +64,14 @@ Threshr is available to jvm projects via [maven central].
<details><summary>Gradle</summary>

```groovy
implementation group: 'com.graqr', name: 'threshr', version: '0.0.13'
implementation group: 'com.graqr', name: 'threshr', version: '0.0.14-SNAPSHOT'
```
</details>

<details><summary>Gradle Kotlin</summary>

```kotlin
implementation("com.graqr:threshr:0.0.13")
implementation("com.graqr:threshr:0.0.14-SNAPSHOT")
```
</details>

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.graqr</groupId>
<artifactId>threshr</artifactId>
<version>0.0.13</version>
<version>0.0.14-SNAPSHOT</version>
<packaging>${packaging}</packaging>

<parent>
Expand Down
101 changes: 88 additions & 13 deletions src/main/java/com/graqr/threshr/Threshr.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.graqr.threshr.model.queryparam.Tcin;
import com.graqr.threshr.model.redsky.product.Product;
import com.graqr.threshr.model.redsky.product.ProductSummaryWithFulfillment;
import com.graqr.threshr.model.redsky.product.Search;
import com.graqr.threshr.model.redsky.product.plp.search.PlpSearchRoot;
import com.graqr.threshr.model.redsky.store.NearbyStores;
import com.graqr.threshr.model.redsky.store.Store;
import io.micronaut.core.async.annotation.SingleResult;
Expand All @@ -16,21 +18,27 @@

import java.util.List;

@Controller("/constructor")
import static com.graqr.threshr.Utils.getSecureRandomString;

@Controller("/constructor") // https://github.com/Graqr/Threshr/issues/147
public class Threshr {

private final ThreshrClient threshrClient;
private final String visitorID;

@Inject
public Threshr(@SuppressWarnings("ClassEscapesDefinedScope") ThreshrClient threshrClient){
public Threshr(@SuppressWarnings("ClassEscapesDefinedScope") ThreshrClient threshrClient) {
this.threshrClient = threshrClient;
visitorID = getSecureRandomString(32);
}

// ------- product summary queries -------

/**
* Query product summaries and their fulfillment options. See {@link ProductSummaryWithFulfillment}
*
* @param targetStore TargetStore object whose inventory is queried for product summaries
* @param tcin TCIN object for one or many product ID(s)
* @param tcin TCIN object for one or many product ID(s)
* @return List of product summaries, one for each ID in the tcin object.
* @throws ThreshrException if no product summaries are returned by the query
*/
Expand All @@ -46,7 +54,7 @@ public List<ProductSummaryWithFulfillment> fetchProductSummaries(TargetStore tar
* Query product summaries and their fulfillment options. See {@link ProductSummaryWithFulfillment}
*
* @param targetStore TargetStore object whose inventory is queried for product summaries
* @param tcin single or many string values for product ID(s)
* @param tcin single or many string values for product ID(s)
* @return List of product summaries, one for each ID in the tcin object.
* @throws ThreshrException if no product summaries are returned by the query
*/
Expand All @@ -56,12 +64,14 @@ public List<ProductSummaryWithFulfillment> fetchProductSummaries(TargetStore tar
return fetchProductSummaries(targetStore, new Tcin(tcin));
}

// ------- pdp queries -------

/**
* Queries the product details page for a given product at a given store.
*
* @param pricingStoreId 4-digit target store identifier
* @param storeId 4-digit target store identifier
* @param tcin Target's internal product id number. aka 'Target Catalog Identification Number'
* @param storeId 4-digit target store identifier
* @param tcin Target's internal product id number. aka 'Target Catalog Identification Number'
* @return Product object matching the given query
* @throws ThreshrException if no Product matching given query is found
*/
Expand All @@ -73,6 +83,68 @@ public Product fetchProductDetails(String pricingStoreId, String storeId, String
.product();
}

//------- plp queries -------

@Get("/product/listings/")
public List<Search> plpQuery(TargetStore pricingStore, String category) throws ThreshrException {
int offset = 0;
List<Search> searchList = new java.util.ArrayList<>();
Search result;
do {
result = plpQuery(pricingStore, category, offset);
searchList.add(result);
offset ++;
} while (result.searchResponse().metadata().totalPages() > offset);
return searchList;
}

/**
* This is product listings query with sensible default values for channel, page and visitorId.
* See{@link PlpSearchRoot}.
*
* @param pricingStore store from which the product listings are to be queried.
* @param category Target's internal product category id.
* @return Search object with non-null product array.
* May include non-null searchSuggestion string array.
* May include non-null SearchResponse object.
* @throws ThreshrException if body of HttpResponse providing the Search object is null
*/
@Get("/product/listings/{offset}")
@SingleResult
public Search plpQuery(TargetStore pricingStore, String category, int offset) throws ThreshrException {
return plpQuery(
pricingStore.getStoreId(),
visitorID,
offset,
category,
"/c/" + category,
null == System.getenv("THRESHR_CHANNEL") ? "WEB" : System.getenv("THRESHR_CHANNEL")
);
}

/**
* This is the lower level query for product listings. See {@link PlpSearchRoot}.
*
* @param pricingStoreId store from which the product listings are to be queried.
* @param visitorId id for the visitor. This could be meaningless, but can't be null.
* @param category Target's internal product category id.
* @param page Seems to be the category value prepended with "/c/"
* @param channel communication through which this api is being called. it's always 'WEB'
* @return Search object with non-null product array.
* May include non-null searchSuggestion string array.
* May include non-null SearchResponse object.
* @throws ThreshrException if body of HttpResponse providing the Search object is null
*/
@Get("/product/listings/{offset}")
@SingleResult
public Search plpQuery(String pricingStoreId, String visitorId, int offset, String category,
String page, String channel) throws ThreshrException {
return checkForNull(threshrClient.getProductListings(pricingStoreId, visitorId, offset, category, page, channel))
.data().search();
}

//------- stores -------

/**
* queries at most 5 stores within 100 miles of a given location
*
Expand All @@ -89,8 +161,8 @@ public NearbyStores getStores(Place place) throws ThreshrException {
/**
* Queries stores relative to a given location
*
* @param place Either a zipcode or a city-state pair of strings. see {@link Place}.
* @param limit max store locations to include in returned NearbyStores object.
* @param place Either a zipcode or a city-state pair of strings. see {@link Place}.
* @param limit max store locations to include in returned NearbyStores object.
* @param within distance from given location to include in search.
* @return NearbyStores object with a list of store objects
* @throws ThreshrException if the returned value is null.
Expand Down Expand Up @@ -122,7 +194,7 @@ public Store getStore(String storeId) throws ThreshrException {
*
* @param storeId 4-digit unique id for a target store
* @param channel communication through which this api is being called.
* @param page source web page on target.com where this api call originates
* @param page source web page on target.com where this api call originates
* @return Store object
* @throws ThreshrException if body of HttpResponse is null
*/
Expand All @@ -135,15 +207,18 @@ public Store getStore(String storeId, String channel, Page page) throws ThreshrE
/**
* Checks if the provided HttpResponse object has a null body and throws a ThreshrException if it does.
*
* @param <T> The type of object expected in the response body.
* @param <T> The type of object expected in the response body.
* @param response The HttpResponse object to check.
* @return The body of the HttpResponse object if it's not null.
* @throws ThreshrException If the response body is null.
*/
private <T> T checkForNull(HttpResponse<T> response) throws ThreshrException {
if (null == response.body()) {
throw new ThreshrException("response body is null or of an unexpected type.\n" + response);
if (null != response.body()) {
return response.body();
}
return response.body();
throw new ThreshrException("response body is null or of an unexpected type.\n" +
"Response Code:" + response.code() + "\n" + response.getStatus());

}

}
39 changes: 34 additions & 5 deletions src/main/java/com/graqr/threshr/ThreshrClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.graqr.threshr.model.queryparam.TargetStore;
import com.graqr.threshr.model.queryparam.Tcin;
import com.graqr.threshr.model.redsky.product.pdp.client.PdpClientRoot;
import com.graqr.threshr.model.redsky.product.plp.search.PlpSearchRoot;
import com.graqr.threshr.model.redsky.product.summary.ProductSummaryRoot;
import com.graqr.threshr.model.redsky.store.location.StoreLocationRoot;
import com.graqr.threshr.model.redsky.store.nearby.NearbyStoreRoot;
Expand All @@ -12,10 +13,8 @@
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.retry.annotation.Retryable;
import jakarta.validation.constraints.Pattern;

import static io.micronaut.http.HttpHeaders.ACCEPT;
import static io.micronaut.http.HttpHeaders.USER_AGENT;

/**
* This is a Micronaut HttpClient which consumes the target corporation's api.
Expand All @@ -24,8 +23,13 @@
* @since 0.0.11
*/
@Client(id = "redsky-api")
@Header(name = USER_AGENT, value = "Micronaut HTTP Client")
@Header(name = ACCEPT, value = "application/vnd.github.v3+json, application/json")
@Header(name = ACCEPT, value = "text/html," +
"application/xhtml+xml," +
"application/xml;q=0.9," +
"image/avif,image/webp," +
"image/png," +
"image/svg+xml," +
"*/*;q=0.8")
interface ThreshrClient {

/**
Expand Down Expand Up @@ -61,9 +65,34 @@ HttpResponse<ProductSummaryRoot> getProductSummary(
HttpResponse<PdpClientRoot> getProductDetails(
@QueryValue("pricing_store_id") String pricingStoreId,
@QueryValue("store_id") String storeId,
@Pattern(regexp = "(\\d{8})|(\\d{9})")
String tcin);

/**
* Queries the 'plp_search_vs' endpoint for a given category at a given store. plp stands for product listing page.
*
* @param pricingStoreId store from which the product listings are to be queried.
* @param visitorId id for the visitor. This could be meaningless, but can't be null.
* @param category Target's internal category id.
* @param page Seems to be the category value prepended with "/c/"
* @param channel communication through which this api is being called. it's always 'WEB'
* @return HttpResponse object containing ProductListings object
*/
@Get("plp_search_v2" +
"?key=${threshr.key}" +
"{&category}" +
"{&offset}" +
"{&channel}" +
"{&page}" +
"{&pricingStoreId}" +
"{&visitorId}")
HttpResponse<PlpSearchRoot> getProductListings(
@QueryValue("pricing_store_id") String pricingStoreId,
@QueryValue("visitor_id") String visitorId,
int offset,
String category,
String page,
String channel);

/**
* returns target stores within a given distance from a location.
*
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/graqr/threshr/UserAgentFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.graqr.threshr;

import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.annotation.ClientFilter;
import io.micronaut.http.annotation.RequestFilter;

@ClientFilter("/**")
public class UserAgentFilter {
@RequestFilter
public void doFilter(MutableHttpRequest<?> request) {
request.header("User-Agent", Utils.getSecureRandomString(32));
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/graqr/threshr/Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.graqr.threshr;

import java.security.SecureRandom;
import java.util.Base64;

class Utils {
/**
* Generates a secure random string of the specified length.
*
* @param length the length of the random string to generate
* @return the generated random string
*/
public static String getSecureRandomString(int length) {
SecureRandom random = new SecureRandom(); // Compliant for security-sensitive use cases
byte[] bytes = new byte[length];
random.nextBytes(bytes);
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
return encoder.encodeToString(bytes);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.graqr.threshr.model.redsky.product;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.serde.annotation.Serdeable;

import java.util.List;
Expand All @@ -9,13 +10,12 @@
public record Product(
@JsonProperty("__typename") String typename,
String tcin,
@Nullable @JsonProperty("original_tcin") String originalTcin,
Category category,
@JsonProperty("ratings_and_reviews") RatingsAndReviews ratingsAndReviews,
Item item,
@JsonProperty("finds_stories")
List<FindsStory> findsStories,
@JsonProperty("finds_posts")
List<FindsPost> findsPosts,
@JsonProperty("finds_stories") List<FindsStory> findsStories,
@Nullable @JsonProperty("finds_posts") List<FindsPost> findsPosts,
Price price,
List<?> promotions) {
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.graqr.threshr.model.redsky.product;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.serde.annotation.Serdeable;

@Serdeable
public record Search(
@JsonProperty("search_recommendations")
SearchRecommendations searchRecommendations,
@JsonProperty("search_response")
SearchResponse searchResponse
SearchResponse searchResponse,
@Nullable
@JsonProperty("search_suggestions")
String[] searchSuggestions,
@Nullable
Product[] products
) {
}
Loading
Loading