Skip to content
Open
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
Expand Up @@ -2031,6 +2031,50 @@ protected static String getResponseBody(Response response) throws ConstructorExc
throw new ConstructorException(errorMessage, errorCode);
}

/**
* Validates and extracts the file extension from a File object for catalog uploads.
* Only .csv, .json, and .jsonl extensions are supported.
*
* @param file the File object containing the actual file
* @param fileName the logical file name (items, variations, item_groups)
* @return the validated file extension (including the dot, e.g., ".csv", ".json", or ".jsonl")
* @throws ConstructorException if the file extension is invalid or missing
*/
private static String getValidatedFileExtension(File file, String fileName)
throws ConstructorException {
if (file == null) {
throw new ConstructorException(
"Invalid file for '" + fileName + "': file cannot be null.");
}

String actualFileName = file.getName();
if (actualFileName == null || actualFileName.isEmpty()) {
throw new ConstructorException(
"Invalid file for '" + fileName + "': file name cannot be empty.");
}

int lastDotIndex = actualFileName.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == actualFileName.length() - 1) {
throw new ConstructorException(
"Invalid file for '"
+ fileName
+ "': file must have .csv, .json, or .jsonl extension. Found: "
+ actualFileName);
}

String extension = actualFileName.substring(lastDotIndex).toLowerCase();

if (!extension.equals(".csv") && !extension.equals(".json") && !extension.equals(".jsonl")) {
throw new ConstructorException(
"Invalid file type for '"
+ fileName
+ "': file must have .csv, .json, or .jsonl extension. Found: "
+ actualFileName);
}

return extension;
}

/**
* Grabs the version number (hard coded ATM)
*
Expand Down Expand Up @@ -2308,9 +2352,12 @@ protected static JSONArray transformItemsAPIV2Response(JSONArray results) {
/**
* Send a full catalog to replace the current one (sync)
*
* @param req the catalog request
* @return a string of JSON
* @throws ConstructorException if the request is invalid.
* Supports CSV, JSON, and JSONL file formats. The file type is automatically
* detected from the file extension (.csv, .json, or .jsonl).
*
* @param req the catalog request containing files with .csv, .json, or .jsonl extensions
* @return a string of JSON containing task information
* @throws ConstructorException if the request is invalid or file extensions are not supported
*/
public String replaceCatalog(CatalogRequest req) throws ConstructorException {
try {
Expand All @@ -2336,10 +2383,11 @@ public String replaceCatalog(CatalogRequest req) throws ConstructorException {
for (Map.Entry<String, File> entry : files.entrySet()) {
String fileName = entry.getKey();
File file = entry.getValue();
String fileExtension = getValidatedFileExtension(file, fileName);

multipartBuilder.addFormDataPart(
fileName,
fileName + ".csv",
fileName + fileExtension,
RequestBody.create(MediaType.parse("application/octet-stream"), file));
}
}
Expand All @@ -2365,9 +2413,12 @@ public String replaceCatalog(CatalogRequest req) throws ConstructorException {
/**
* Send a partial catalog to update specific items (delta)
*
* @param req the catalog request
* @return a string of JSON
* @throws ConstructorException if the request is invalid.
* Supports CSV, JSON, and JSONL file formats. The file type is automatically
* detected from the file extension (.csv, .json, or .jsonl).
*
* @param req the catalog request containing files with .csv, .json, or .jsonl extensions
* @return a string of JSON containing task information
* @throws ConstructorException if the request is invalid or file extensions are not supported
*/
public String updateCatalog(CatalogRequest req) throws ConstructorException {
try {
Expand All @@ -2393,10 +2444,11 @@ public String updateCatalog(CatalogRequest req) throws ConstructorException {
for (Map.Entry<String, File> entry : files.entrySet()) {
String fileName = entry.getKey();
File file = entry.getValue();
String fileExtension = getValidatedFileExtension(file, fileName);

multipartBuilder.addFormDataPart(
fileName,
fileName + ".csv",
fileName + fileExtension,
RequestBody.create(MediaType.parse("application/octet-stream"), file));
}
}
Expand All @@ -2423,9 +2475,12 @@ public String updateCatalog(CatalogRequest req) throws ConstructorException {
/**
* Send a patch delta catalog to update specific items (delta)
*
* @param req the catalog request
* @return a string of JSON
* @throws ConstructorException if the request is invalid.
* Supports CSV, JSON, and JSONL file formats. The file type is automatically
* detected from the file extension (.csv, .json, or .jsonl).
*
* @param req the catalog request containing files with .csv, .json, or .jsonl extensions
* @return a string of JSON containing task information
* @throws ConstructorException if the request is invalid or file extensions are not supported
*/
public String patchCatalog(CatalogRequest req) throws ConstructorException {
try {
Expand Down Expand Up @@ -2456,10 +2511,11 @@ public String patchCatalog(CatalogRequest req) throws ConstructorException {
for (Map.Entry<String, File> entry : files.entrySet()) {
String fileName = entry.getKey();
File file = entry.getValue();
String fileExtension = getValidatedFileExtension(file, fileName);

multipartBuilder.addFormDataPart(
fileName,
fileName + ".csv",
fileName + fileExtension,
RequestBody.create(MediaType.parse("application/octet-stream"), file));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,213 @@ public void PatchCatalogWithItemsAndVariationsAndItemGroupsFilesShouldReturnTask
assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

// JSONL Format Tests

@Test
public void ReplaceCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/jsonl/items.jsonl"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.replaceCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

@Test
public void UpdateCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/jsonl/items.jsonl"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.updateCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

@Test
public void PatchCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/jsonl/items.jsonl"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.patchCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

// Invalid Extension Tests

@Test
public void ReplaceCatalogWithInvalidExtensionShouldError() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/invalid/items.txt"));

CatalogRequest req = new CatalogRequest(files, "Products");

thrown.expect(ConstructorException.class);
thrown.expectMessage("Invalid file type for 'items'");
thrown.expectMessage("must have .csv or .jsonl extension");
constructor.replaceCatalog(req);
}

@Test
public void UpdateCatalogWithNoExtensionShouldError() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/invalid/items"));

CatalogRequest req = new CatalogRequest(files, "Products");

thrown.expect(ConstructorException.class);
thrown.expectMessage("Invalid file for 'items'");
thrown.expectMessage("must have .csv or .jsonl extension");
constructor.updateCatalog(req);
}

@Test
public void PatchCatalogWithInvalidExtensionShouldError() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/invalid/items.txt"));

CatalogRequest req = new CatalogRequest(files, "Products");

thrown.expect(ConstructorException.class);
thrown.expectMessage("Invalid file type for 'items'");
thrown.expectMessage("must have .csv or .jsonl extension");
constructor.patchCatalog(req);
}

// Edge Case Tests

@Test
public void ReplaceCatalogWithMixedFileTypesShouldSucceed() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/csv/items.csv"));
files.put("variations", new File("src/test/resources/jsonl/variations.jsonl"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.replaceCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

@Test
public void UpdateCatalogWithJsonlVariationsAndItemGroupsShouldSucceed() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("variations", new File("src/test/resources/jsonl/variations.jsonl"));
files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.updateCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

@Test
public void PatchCatalogWithAllJsonlFilesShouldSucceed() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/jsonl/items.jsonl"));
files.put("variations", new File("src/test/resources/jsonl/variations.jsonl"));
files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.patchCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

// JSON Format Tests

@Test
public void ReplaceCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/json/items.json"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.replaceCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

@Test
public void UpdateCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/json/items.json"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.updateCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

@Test
public void PatchCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/json/items.json"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.patchCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}

@Test
public void ReplaceCatalogWithMixedCsvJsonJsonlShouldSucceed() throws Exception {
ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null);
Map<String, File> files = new HashMap<String, File>();

files.put("items", new File("src/test/resources/csv/items.csv"));
files.put("variations", new File("src/test/resources/json/variations.json"));
files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl"));

CatalogRequest req = new CatalogRequest(files, "Products");
String response = constructor.replaceCatalog(req);
JSONObject jsonObj = new JSONObject(response);

assertTrue("task_id exists", jsonObj.has("task_id") == true);
assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true);
}
}
1 change: 1 addition & 0 deletions constructorio-client/src/test/resources/invalid/items
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has no extension for testing validation.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a text file with invalid extension for catalog upload testing.
23 changes: 23 additions & 0 deletions constructorio-client/src/test/resources/json/items.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"id": "item1",
"value": "Product 1",
"data": {
"url": "https://example.com/product1"
}
},
{
"id": "item2",
"value": "Product 2",
"data": {
"url": "https://example.com/product2"
}
},
{
"id": "item3",
"value": "Product 3",
"data": {
"url": "https://example.com/product3"
}
}
]
18 changes: 18 additions & 0 deletions constructorio-client/src/test/resources/json/variations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"id": "var1",
"item_id": "item1",
"value": "Product 1 Variation A",
"data": {
"color": "red"
}
},
{
"id": "var2",
"item_id": "item1",
"value": "Product 1 Variation B",
"data": {
"color": "blue"
}
}
]
Loading