diff --git a/src/main/java/com/mindee/http/MindeeApiV2.java b/src/main/java/com/mindee/http/MindeeApiV2.java index 4ec5f5804..59fa37c02 100644 --- a/src/main/java/com/mindee/http/MindeeApiV2.java +++ b/src/main/java/com/mindee/http/MindeeApiV2.java @@ -3,6 +3,7 @@ import com.mindee.InferenceParameters; import com.mindee.input.LocalInputSource; import com.mindee.input.URLInputSource; +import com.mindee.parsing.v2.ErrorResponse; import com.mindee.parsing.v2.InferenceResponse; import com.mindee.parsing.v2.JobResponse; import java.io.IOException; @@ -44,4 +45,17 @@ public abstract JobResponse reqGetJob( * @param inferenceId ID of the inference to poll. */ abstract public InferenceResponse reqGetInference(String inferenceId); + + /** + * Creates an "unknown error" response from an HTTP status code. + */ + protected ErrorResponse makeUnknownError(int statusCode) { + return new ErrorResponse( + "Unknown Error", + "The server returned an Unknown error.", + statusCode, + statusCode + "-000", + null + ); + } } diff --git a/src/main/java/com/mindee/http/MindeeHttpApiV2.java b/src/main/java/com/mindee/http/MindeeHttpApiV2.java index 724e37180..fdb895912 100644 --- a/src/main/java/com/mindee/http/MindeeHttpApiV2.java +++ b/src/main/java/com/mindee/http/MindeeHttpApiV2.java @@ -236,7 +236,7 @@ private MindeeHttpExceptionV2 getHttpError(ClassicHttpResponse response) { ErrorResponse err = mapper.readValue(rawBody, ErrorResponse.class); if (err.getDetail() == null) { - err = new ErrorResponse("Unknown error", response.getCode()); + err = makeUnknownError(response.getCode()); } return new MindeeHttpExceptionV2(err.getStatus(), err.getDetail()); @@ -321,10 +321,10 @@ private R deserializeOrThrow( try { err = mapper.readValue(body, ErrorResponse.class); if (err.getDetail() == null) { - err = new ErrorResponse("Unknown error", httpStatus); + err = makeUnknownError(httpStatus); } } catch (Exception ignored) { - err = new ErrorResponse("Unknown error", httpStatus); + err = makeUnknownError(httpStatus); } throw new MindeeHttpExceptionV2(err.getStatus(), err.getDetail()); } diff --git a/src/main/java/com/mindee/parsing/v2/ErrorItem.java b/src/main/java/com/mindee/parsing/v2/ErrorItem.java new file mode 100644 index 000000000..8d70edad1 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/ErrorItem.java @@ -0,0 +1,30 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Error item model. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public final class ErrorItem { + /** + * A JSON Pointer to the location of the body property. + */ + @JsonProperty("pointer") + private String pointer; + + /** + * Explicit information on the issue. + */ + @JsonProperty("detail") + private String detail; +} diff --git a/src/main/java/com/mindee/parsing/v2/ErrorResponse.java b/src/main/java/com/mindee/parsing/v2/ErrorResponse.java index 990b07f0b..bd81b579f 100644 --- a/src/main/java/com/mindee/parsing/v2/ErrorResponse.java +++ b/src/main/java/com/mindee/parsing/v2/ErrorResponse.java @@ -2,13 +2,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; /** - * Error information from the API. + * Error response detailing a problem. The format adheres to RFC 9457. */ @Getter @EqualsAndHashCode @@ -17,20 +18,38 @@ @NoArgsConstructor public final class ErrorResponse { /** - * Detail relevant to the error. + * A short, human-readable summary of the problem. + */ + @JsonProperty("title") + private String title; + + /** + * A human-readable explanation specific to the occurrence of the problem. */ @JsonProperty("detail") private String detail; /** - * HTTP error code. + * The HTTP status code returned by the server. */ @JsonProperty("status") private int status; + /** + * A machine-readable code specific to the occurrence of the problem. + */ + @JsonProperty("code") + private String code; + + /** + * The HTTP status code returned by the server. + */ + @JsonProperty("errors") + private List errors; + /** For prettier display. */ @Override public String toString() { - return "HTTP Status: " + status + " - " + detail; + return "HTTP " + status + " - " + title + " :: " + code + " - " + detail; } } diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResult.java b/src/main/java/com/mindee/parsing/v2/InferenceResult.java index 7002897bf..185cc4782 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceResult.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResult.java @@ -20,17 +20,23 @@ public final class InferenceResult { /** - * Model fields. + * Extracted fields, the key corresponds to the field's name in the data schema. */ @JsonProperty("fields") private InferenceFields fields; /** - * Options. + * Raw text extracted from all pages in the document. */ @JsonProperty("raw_text") private RawText rawText; + /** + * RAG metadata. + */ + @JsonProperty("rag") + private RagMetadata rag; + @Override public String toString() { StringJoiner joiner = new StringJoiner("\n"); diff --git a/src/main/java/com/mindee/parsing/v2/RagMetadata.java b/src/main/java/com/mindee/parsing/v2/RagMetadata.java new file mode 100644 index 000000000..afadb2e3e --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/RagMetadata.java @@ -0,0 +1,22 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * RAG metadata. + */ +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public final class RagMetadata { + /** + * The UUID of the matched document used during the RAG operation. + */ + @JsonProperty("retrieved_document_id") + private String retrievedDocumentId; +} diff --git a/src/main/java/com/mindee/parsing/v2/RawText.java b/src/main/java/com/mindee/parsing/v2/RawText.java index 5720d41ed..e8df9ed97 100644 --- a/src/main/java/com/mindee/parsing/v2/RawText.java +++ b/src/main/java/com/mindee/parsing/v2/RawText.java @@ -16,7 +16,7 @@ @AllArgsConstructor @NoArgsConstructor public class RawText { - /* + /** * Page Number the text was found on. */ @JsonProperty("pages") diff --git a/src/test/java/com/mindee/MindeeClientV2IT.java b/src/test/java/com/mindee/MindeeClientV2IT.java index c34e1bf74..624f997a9 100644 --- a/src/test/java/com/mindee/MindeeClientV2IT.java +++ b/src/test/java/com/mindee/MindeeClientV2IT.java @@ -20,7 +20,7 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Tag("integration") -@DisplayName("MindeeClientV2 – integration tests (V2)") +@DisplayName("MindeeV2 – Integration Tests") class MindeeClientV2IT { private MindeeClientV2 mindeeClient; diff --git a/src/test/java/com/mindee/MindeeClientV2Test.java b/src/test/java/com/mindee/MindeeClientV2Test.java index eb4531e64..b0a8b1b85 100644 --- a/src/test/java/com/mindee/MindeeClientV2Test.java +++ b/src/test/java/com/mindee/MindeeClientV2Test.java @@ -19,7 +19,7 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -@DisplayName("MindeeClientV2 – client / API interaction tests") +@DisplayName("MindeeV2 – Client and API Tests") class MindeeClientV2Test { /** * Creates a fully mocked MindeeClientV2. diff --git a/src/test/java/com/mindee/TestingUtilities.java b/src/test/java/com/mindee/TestingUtilities.java index eec90e651..71872ca86 100644 --- a/src/test/java/com/mindee/TestingUtilities.java +++ b/src/test/java/com/mindee/TestingUtilities.java @@ -18,6 +18,10 @@ public static Path getV1ResourcePath(String filePath) { return Paths.get("src/test/resources/v1/" + filePath); } + public static Path getV2ResourcePath(String filePath) { + return Paths.get("src/test/resources/v2/" + filePath); + } + public static String getV1ResourcePathString(String filePath) { return getV1ResourcePath(filePath).toString(); } diff --git a/src/test/java/com/mindee/parsing/v2/InferenceTest.java b/src/test/java/com/mindee/parsing/v2/InferenceTest.java index fa448a88d..2c82d9626 100644 --- a/src/test/java/com/mindee/parsing/v2/InferenceTest.java +++ b/src/test/java/com/mindee/parsing/v2/InferenceTest.java @@ -12,43 +12,41 @@ import com.mindee.parsing.v2.field.ObjectField; import com.mindee.parsing.v2.field.DynamicField.FieldType; import java.io.IOException; +import java.nio.file.Files; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import static com.mindee.TestingUtilities.getV2ResourcePath; import static org.junit.jupiter.api.Assertions.*; -@DisplayName("InferenceV2 – field integrity checks") +@DisplayName("MindeeV2 - Inference Tests") class InferenceTest { - private InferenceResponse loadFromResource(String resourcePath) throws IOException { - LocalResponse localResponse = new LocalResponse( - InferenceTest.class.getClassLoader().getResourceAsStream(resourcePath) - ); + private InferenceResponse loadInference(String filePath) throws IOException { + LocalResponse localResponse = new LocalResponse(getV2ResourcePath(filePath)); return localResponse.deserializeResponse(InferenceResponse.class); } private String readFileAsString(String path) throws IOException { - byte[] encoded = IOUtils.toByteArray(Objects.requireNonNull(InferenceTest.class.getClassLoader().getResourceAsStream(path))); + byte[] encoded = Files.readAllBytes(getV2ResourcePath(path)); return new String(encoded); } @Nested - @DisplayName("When the async prediction is blank") - class BlankPrediction { + @DisplayName("Inference on blank file") + class BlankPredictionTest { @Test @DisplayName("all properties must be valid") void asyncPredict_whenEmpty_mustHaveValidProperties() throws IOException { - InferenceResponse response = loadFromResource("v2/products/financial_document/blank.json"); + InferenceResponse response = loadInference("products/financial_document/blank.json"); InferenceFields fields = response.getInference().getResult().getFields(); assertEquals(21, fields.size(), "Expected 21 fields"); @@ -94,13 +92,13 @@ void asyncPredict_whenEmpty_mustHaveValidProperties() throws IOException { } @Nested - @DisplayName("When the async prediction is complete") - class CompletePrediction { + @DisplayName("Inference on filled file") + class CompletePredictionTest { @Test @DisplayName("every exposed property must be valid and consistent") void asyncPredict_whenComplete_mustExposeAllProperties() throws IOException { - InferenceResponse response = loadFromResource("v2/products/financial_document/complete.json"); + InferenceResponse response = loadInference("products/financial_document/complete.json"); Inference inference = response.getInference(); assertNotNull(inference, "Inference must not be null"); assertEquals("12345678-1234-1234-1234-123456789abc", inference.getId(), "Inference ID mismatch"); @@ -155,12 +153,12 @@ void asyncPredict_whenComplete_mustExposeAllProperties() throws IOException { @Nested @DisplayName("deep_nested_fields.json") - class DeepNestedFields { + class DeepNestedFieldsTest { @Test @DisplayName("all nested structures must be typed correctly") void deepNestedFields_mustExposeCorrectTypes() throws IOException { - InferenceResponse resp = loadFromResource("v2/inference/deep_nested_fields.json"); + InferenceResponse resp = loadInference("inference/deep_nested_fields.json"); Inference inf = resp.getInference(); assertNotNull(inf); @@ -191,7 +189,7 @@ void deepNestedFields_mustExposeCorrectTypes() throws IOException { @Nested @DisplayName("standard_field_types.json") - class StandardFieldTypes { + class StandardFieldTypesTest { private void testSimpleFieldString(SimpleField field) { assertNotNull(field); @@ -204,7 +202,7 @@ private void testSimpleFieldString(SimpleField field) { @Test @DisplayName("simple fields must be recognised") void standardFieldTypes_mustExposeSimpleFieldValues() throws IOException { - InferenceResponse response = loadFromResource("v2/inference/standard_field_types.json"); + InferenceResponse response = loadInference("inference/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -254,7 +252,7 @@ void standardFieldTypes_mustExposeSimpleFieldValues() throws IOException { @Test @DisplayName("simple list fields must be recognised") void standardFieldTypes_mustExposeSimpleListFieldValues() throws IOException { - InferenceResponse response = loadFromResource("v2/inference/standard_field_types.json"); + InferenceResponse response = loadInference("inference/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -297,7 +295,7 @@ private void testObjectSubFieldSimpleString(String fieldName, SimpleField subFie @Test @DisplayName("object list fields must be recognised") void standardFieldTypes_mustExposeObjectListFieldValues() throws IOException { - InferenceResponse response = loadFromResource("v2/inference/standard_field_types.json"); + InferenceResponse response = loadInference("inference/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -347,7 +345,7 @@ void standardFieldTypes_mustExposeObjectListFieldValues() throws IOException { @Test @DisplayName("simple / object / list variants must be recognised") void standardFieldTypes_mustExposeObjectFieldValues() throws IOException { - InferenceResponse response = loadFromResource("v2/inference/standard_field_types.json"); + InferenceResponse response = loadInference("inference/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -382,7 +380,7 @@ void standardFieldTypes_mustExposeObjectFieldValues() throws IOException { @Test @DisplayName("allow getting fields using generics") void standardFieldTypes_getWithGenerics() throws IOException { - InferenceResponse response = loadFromResource("v2/inference/standard_field_types.json"); + InferenceResponse response = loadInference("inference/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); InferenceFields fields = inference.getResult().getFields(); @@ -418,7 +416,7 @@ void standardFieldTypes_getWithGenerics() throws IOException { @Test @DisplayName("confidence and locations must be usable") void standardFieldTypes_confidenceAndLocations() throws IOException { - InferenceResponse response = loadFromResource("v2/inference/standard_field_types.json"); + InferenceResponse response = loadInference("inference/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -452,13 +450,13 @@ void standardFieldTypes_confidenceAndLocations() throws IOException { } @Nested - @DisplayName("raw_texts.json") - class RawTexts { + @DisplayName("Raw Text") + class RawTextTest { @Test @DisplayName("raw texts option must be parsed and exposed") void rawTexts_mustBeAccessible() throws IOException { - InferenceResponse response = loadFromResource("v2/inference/raw_texts.json"); + InferenceResponse response = loadInference("inference/raw_texts.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -469,9 +467,10 @@ void rawTexts_mustBeAccessible() throws IOException { assertFalse(activeOptions.getPolygon()); assertFalse(activeOptions.getConfidence()); + assertNull(inference.getResult().getRag()); + RawText rawText = inference.getResult().getRawText(); assertEquals(2, rawText.getPages().size()); - String documentText = rawText.toString(); for (RawTextPage page : rawText.getPages()) { assertNotNull(page.getContent()); @@ -484,13 +483,42 @@ void rawTexts_mustBeAccessible() throws IOException { } @Nested - @DisplayName("rst display") + @DisplayName("Rag Metadata") + class RagMetadataTest { + + @Test + @DisplayName("RAG metadata when matched") + void rag_mustBeFilled_whenMatched() throws IOException { + InferenceResponse response = loadInference("inference/rag_matched.json"); + Inference inference = response.getInference(); + assertNotNull(inference); + + RagMetadata rag = inference.getResult().getRag(); + assertNotNull(rag); + assertEquals("12345abc-1234-1234-1234-123456789abc", rag.getRetrievedDocumentId()); + } + + @Test + @DisplayName("RAG metadata when not matched") + void rag_mustBeNull_whenNotMatched() throws IOException { + InferenceResponse response = loadInference("inference/rag_not_matched.json"); + Inference inference = response.getInference(); + assertNotNull(inference); + + RagMetadata rag = inference.getResult().getRag(); + assertNotNull(rag); + assertNull(rag.getRetrievedDocumentId()); + } + } + + @Nested + @DisplayName("RST Display") class RstDisplay { @Test @DisplayName("rst display must be parsed and exposed") void rstDisplay_mustBeAccessible() throws IOException { - InferenceResponse resp = loadFromResource("v2/inference/standard_field_types.json"); - String rstRef = readFileAsString("v2/inference/standard_field_types.rst"); + InferenceResponse resp = loadInference("inference/standard_field_types.json"); + String rstRef = readFileAsString("inference/standard_field_types.rst"); Inference inf = resp.getInference(); assertNotNull(inf); assertEquals(rstRef, resp.getInference().toString()); diff --git a/src/test/java/com/mindee/parsing/v2/JobTest.java b/src/test/java/com/mindee/parsing/v2/JobTest.java new file mode 100644 index 000000000..bec940ac2 --- /dev/null +++ b/src/test/java/com/mindee/parsing/v2/JobTest.java @@ -0,0 +1,52 @@ +package com.mindee.parsing.v2; + +import com.mindee.input.LocalResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import java.io.IOException; +import static com.mindee.TestingUtilities.getV2ResourcePath; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DisplayName("MindeeV2 - Job Tests") +public class JobTest { + private JobResponse loadJob(String filePath) throws IOException { + LocalResponse localResponse = new LocalResponse(getV2ResourcePath(filePath)); + return localResponse.deserializeResponse(JobResponse.class); + } + + @Nested + @DisplayName("When the Job is OK") + class OkTest { + @Test + @DisplayName("properties must be valid") + void whenProcessing_mustHaveValidProperties() throws IOException { + JobResponse response = loadJob("job/ok_processing.json"); + Job job = response.getJob(); + assertNotNull(job); + assertEquals("Processing", job.getStatus()); + assertNull(job.getError()); + } + } + + @Nested + @DisplayName("When the Job fails") + class FailTest { + @Test + @DisplayName("HTTP 422 properties must be valid") + void when422_mustHaveValidProperties() throws IOException { + JobResponse response = loadJob("job/fail_422.json"); + Job job = response.getJob(); + assertNotNull(job); + ErrorResponse jobError = job.getError(); + assertNotNull(jobError); + assertEquals(422, jobError.getStatus()); + assertEquals("Invalid fields in form", jobError.getTitle()); + assertEquals("422-001", jobError.getCode()); + assertEquals("One or more fields failed validation.", jobError.getDetail()); + assertEquals(1, jobError.getErrors().size()); + } + } +}