diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 458d5ced..8d0d7797 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: java-version: + - "21" - "11" - "8" distribution: diff --git a/.github/workflows/_test-integrations.yml b/.github/workflows/_test-integrations.yml index fe4bed41..bca2f454 100644 --- a/.github/workflows/_test-integrations.yml +++ b/.github/workflows/_test-integrations.yml @@ -12,6 +12,7 @@ jobs: strategy: matrix: java-version: + - "21" - "11" - "8" distribution: diff --git a/pom.xml b/pom.xml index 8366754b..97aefa4a 100644 --- a/pom.xml +++ b/pom.xml @@ -133,6 +133,20 @@ 8 + + org.apache.maven.plugins + maven-surefire-plugin + ${org.apache.maven.surfire.version} + + + **/*Test.java + + + ${surefire.addOpens} + -Djunit.jupiter.extensions.autodetection.enabled=true + + + org.sonatype.central central-publishing-maven-plugin @@ -148,6 +162,22 @@ + + jdk-9-plus + + [9,) + + + + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED + --add-opens java.base/java.io=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + + + release @@ -178,16 +208,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - ${org.apache.maven.surfire.version} - - - **/*Test.java - - - org.apache.maven.plugins maven-gpg-plugin @@ -361,6 +381,18 @@ test ${org.mockito.inline.version} + + net.bytebuddy + byte-buddy + 1.15.11 + test + + + net.bytebuddy + byte-buddy-agent + 1.15.11 + test + com.github.tomakehurst wiremock-jre8 @@ -412,12 +444,13 @@ 1.8.2 2.2 1.5.3.Final - 4.6.1 - 4.5.1 + 4.11.0 + 4.11.0 0.2.0 1.18.32 2.0.17 UTF-8 + 2.35.2 diff --git a/src/main/java/com/mindee/parsing/v2/field/ObjectField.java b/src/main/java/com/mindee/parsing/v2/field/ObjectField.java index 8798d6b2..e3c2a12d 100644 --- a/src/main/java/com/mindee/parsing/v2/field/ObjectField.java +++ b/src/main/java/com/mindee/parsing/v2/field/ObjectField.java @@ -42,6 +42,97 @@ public LinkedHashMap getSimpleFields() throws IllegalStateE return simpleFields; } + /** + * Retrieves all subfields from the {@code fields} map as a {@link LinkedHashMap} of + * {@code ListField} objects, keyed by their field names. + * + * @return a {@link LinkedHashMap} containing the field names as keys and their corresponding + * {@code ListField} instances as values (only includes fields that are list fields) + */ + public LinkedHashMap getListFields() { + LinkedHashMap listFields = new LinkedHashMap<>(); + if (fields != null) { + for (String fieldName : fields.keySet()) { + DynamicField field = fields.get(fieldName); + if (field != null && field.getType() == DynamicField.FieldType.LIST_FIELD) { + listFields.put(fieldName, field.getListField()); + } + } + } + return listFields; + } + + /** + * Retrieves all subfields from the {@code fields} map as a {@link LinkedHashMap} of + * {@code ObjectField} objects, keyed by their field names. + * + * @return a {@link LinkedHashMap} containing the field names as keys and their corresponding + * {@code ObjectField} objects as values + */ + public LinkedHashMap getObjectFields() { + LinkedHashMap objectFields = new LinkedHashMap<>(); + if (fields != null) { + for (String fieldName : fields.keySet()) { + DynamicField field = fields.get(fieldName); + if (field != null && field.getType() == DynamicField.FieldType.OBJECT_FIELD) { + objectFields.put(fieldName, field.getObjectField()); + } + } + } + return objectFields; + } + + /** + * Retrieves a single sub-field from the {@code fields} map as a {@link SimpleField}. + * + * @param fieldKey the name/key of the field to retrieve + * @return the corresponding {@link SimpleField}, or {@code null} if {@code fields} is + * {@code null} + * or if no field exists for {@code fieldKey} (depending on {@code fields}' behavior) + * @throws IllegalStateException if the referenced field exists but is not of type + * {@code SIMPLE_FIELD} + */ + public SimpleField getSimpleField(String fieldKey) throws IllegalStateException { + if (fields == null) { + return null; + } + return fields.getSimpleField(fieldKey); + } + + /** + * Retrieves a list sub-field from the {@code fields} map as a {@link ListField}. + * + * @param fieldKey the name/key of the field to retrieve + * @return the corresponding {@link ListField}, or {@code null} if {@code fields} is + * {@code null} + * or if no field exists for {@code fieldKey} (depending on {@code fields}' behavior) + * @throws IllegalStateException if the referenced field exists but is not of type + * {@code LIST_FIELD} + */ + public ListField getListField(String fieldKey) { + if (fields == null) { + return null; + } + return fields.getListField(fieldKey); + } + + /** + * Retrieves an object sub-field from the {@code fields} map as a {@link ObjectField}. + * + * @param fieldKey the name/key of the field to retrieve + * @return the corresponding {@link ObjectField}, or {@code null} if {@code fields} is + * {@code null} + * or if no field exists for {@code fieldKey} (depending on {@code fields}' behavior) + * @throws IllegalStateException if the referenced field exists but is not of type + * {@code OBJECT_FIELD} + */ + public ObjectField getObjectField(String fieldKey) { + if (fields == null) { + return null; + } + return fields.getObjectField(fieldKey); + } + @Override public String toString() { return "\n" + (fields != null ? fields.toString(1) : ""); diff --git a/src/test/java/com/mindee/MindeeClientV2IT.java b/src/test/java/com/mindee/MindeeClientV2IT.java index 296690cf..106582b1 100644 --- a/src/test/java/com/mindee/MindeeClientV2IT.java +++ b/src/test/java/com/mindee/MindeeClientV2IT.java @@ -87,7 +87,7 @@ void parseFile_emptyMultiPage_mustSucceed() throws IOException, InterruptedExcep @DisplayName("Filled, single-page image – enqueue & parse must succeed") void parseFile_filledSinglePage_mustSucceed() throws IOException, InterruptedException { LocalInputSource source = new LocalInputSource( - getV2ResourcePath("products/financial_document/default_sample.jpg") + getV2ResourcePath("products/extraction/financial_document/default_sample.jpg") ); InferenceParameters params = InferenceParameters @@ -135,14 +135,16 @@ void parseFile_filledSinglePage_mustSucceed() throws IOException, InterruptedExc @DisplayName("Data Schema Replace – enqueue & parse must succeed") void parseFile_dataSchemaReplace_mustSucceed() throws IOException, InterruptedException { LocalInputSource source = new LocalInputSource( - getV2ResourcePath("products/financial_document/default_sample.jpg") + getV2ResourcePath("products/extraction/financial_document/default_sample.jpg") ); InferenceParameters params = InferenceParameters .builder(modelId) .rag(false) .alias("java-integration-test_data-schema-replace") - .dataSchema(readFileAsString(getV2ResourcePath("inference/data_schema_replace_param.json"))) + .dataSchema( + readFileAsString(getV2ResourcePath("products/extraction/data_schema_replace_param.json")) + ) .build(); InferenceResponse response = mindeeClient.enqueueAndGetInference(source, params); diff --git a/src/test/java/com/mindee/MindeeClientV2Test.java b/src/test/java/com/mindee/MindeeClientV2Test.java index 67a5a017..d3e41f96 100644 --- a/src/test/java/com/mindee/MindeeClientV2Test.java +++ b/src/test/java/com/mindee/MindeeClientV2Test.java @@ -1,9 +1,13 @@ package com.mindee; import static com.mindee.TestingUtilities.getResourcePath; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atMostOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -87,7 +91,9 @@ void document_getInference_async() throws IOException { MindeeApiV2 predictable = Mockito.mock(MindeeApiV2.class); String json = FileUtils - .readFileToString(getResourcePath("v2/products/financial_document/complete.json").toFile()); + .readFileToString( + getResourcePath("v2/products/extraction/financial_document/complete.json").toFile() + ); ObjectMapper mapper = new ObjectMapper(); mapper.findAndRegisterModules(); @@ -129,7 +135,7 @@ class DeserializeResponse { @DisplayName("parses local JSON and exposes correct field values") void inference_loadsLocally() throws IOException { LocalResponse localResponse = new LocalResponse( - getResourcePath("v2/products/financial_document/complete.json") + getResourcePath("v2/products/extraction/financial_document/complete.json") ); InferenceResponse loaded = localResponse.deserializeResponse(InferenceResponse.class); diff --git a/src/test/java/com/mindee/MindeeSettingsTest.java b/src/test/java/com/mindee/MindeeSettingsTest.java index 4ee59da1..46f76d69 100644 --- a/src/test/java/com/mindee/MindeeSettingsTest.java +++ b/src/test/java/com/mindee/MindeeSettingsTest.java @@ -11,7 +11,7 @@ public class MindeeSettingsTest { @SetEnvironmentVariable(key = "MINDEE_API_URL", value = "https://example.com") void setEnvironmentVariablesAndEmptyParams() { MindeeSettings settings = new MindeeSettings("", ""); - Assertions.assertEquals(settings.getApiKey().orElse(""), "abcd"); - Assertions.assertEquals(settings.getBaseUrl(), "https://example.com"); + Assertions.assertEquals("abcd", settings.getApiKey().orElse("")); + Assertions.assertEquals("https://example.com", settings.getBaseUrl()); } } diff --git a/src/test/java/com/mindee/input/LocalResponseV2Test.java b/src/test/java/com/mindee/input/LocalResponseV2Test.java index b33eab2d..d7066df9 100644 --- a/src/test/java/com/mindee/input/LocalResponseV2Test.java +++ b/src/test/java/com/mindee/input/LocalResponseV2Test.java @@ -19,12 +19,12 @@ public class LocalResponseV2Test { /** * Real signature using fake secret key. */ - String signature = "1df388c992d87897fe61dfc56c444c58fc3c7369c31e2b5fd20d867695e93e85"; + String signature = "e51bdf80f1a08ed44ee161100fc30a25cb35b4ede671b0a575dc9064a3f5dbf1"; /** * File which the signature applies to. */ - Path filePath = getV2ResourcePath("inference/standard_field_types.json"); + Path filePath = getV2ResourcePath("products/extraction/standard_field_types.json"); protected void assertLocalResponse(LocalResponse localResponse) { Assertions.assertNotNull(localResponse.getFile()); diff --git a/src/test/java/com/mindee/parsing/v2/InferenceTest.java b/src/test/java/com/mindee/parsing/v2/InferenceTest.java index 411617e5..58014332 100644 --- a/src/test/java/com/mindee/parsing/v2/InferenceTest.java +++ b/src/test/java/com/mindee/parsing/v2/InferenceTest.java @@ -2,7 +2,13 @@ import static com.mindee.TestingUtilities.getV2ResourcePath; import static com.mindee.TestingUtilities.readFileAsString; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.mindee.geometry.Point; import com.mindee.geometry.Polygon; @@ -40,7 +46,9 @@ class BlankPredictionTest { @Test @DisplayName("all properties must be valid") void asyncPredict_whenEmpty_mustHaveValidProperties() throws IOException { - InferenceResponse response = loadInference("products/financial_document/blank.json"); + InferenceResponse response = loadInference( + "products/extraction/financial_document/blank.json" + ); InferenceFields fields = response.getInference().getResult().getFields(); assertEquals(21, fields.size(), "Expected 21 fields"); @@ -92,7 +100,9 @@ class CompletePredictionTest { @Test @DisplayName("every exposed property must be valid and consistent") void asyncPredict_whenComplete_mustExposeAllProperties() throws IOException { - InferenceResponse response = loadInference("products/financial_document/complete.json"); + InferenceResponse response = loadInference( + "products/extraction/financial_document/complete.json" + ); Inference inference = response.getInference(); assertNotNull(inference); assertEquals("12345678-1234-1234-1234-123456789abc", inference.getId()); @@ -162,7 +172,7 @@ class DeepNestedFieldsTest { @Test @DisplayName("all nested structures must be typed correctly") void deepNestedFields_mustExposeCorrectTypes() throws IOException { - InferenceResponse resp = loadInference("inference/deep_nested_fields.json"); + InferenceResponse resp = loadInference("products/extraction/deep_nested_fields.json"); Inference inf = resp.getInference(); assertNotNull(inf); @@ -171,9 +181,29 @@ void deepNestedFields_mustExposeCorrectTypes() throws IOException { assertNotNull(root.get("field_object").getObjectField()); ObjectField fieldObject = root.get("field_object").getObjectField(); + assertNotNull(fieldObject.getObjectField("sub_object_object")); InferenceFields lvl1 = fieldObject.getFields(); assertNotNull(lvl1.get("sub_object_list").getListField()); assertNotNull(lvl1.get("sub_object_object").getObjectField()); + assertEquals(1, lvl1.get("sub_object_object").getObjectField().getListFields().size()); + assertEquals(1, lvl1.get("sub_object_object").getObjectField().getObjectFields().size()); + assertEquals( + 1, + lvl1 + .get("sub_object_object") + .getObjectField() + .getListField("sub_object_object_sub_object_list") + .getItems() + .size() + ); + assertEquals( + "value_6", + lvl1 + .get("sub_object_object") + .getObjectField() + .getSimpleField("sub_object_object_sub_object_simple") + .getStringValue() + ); ObjectField subObjectObject = lvl1.get("sub_object_object").getObjectField(); InferenceFields lvl2 = subObjectObject.getFields(); @@ -208,7 +238,7 @@ private void testSimpleFieldString(SimpleField field) { @Test @DisplayName("simple fields must be recognised") void standardFieldTypes_mustExposeSimpleFieldValues() throws IOException { - InferenceResponse response = loadInference("inference/standard_field_types.json"); + InferenceResponse response = loadInference("products/extraction/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -265,7 +295,7 @@ void standardFieldTypes_mustExposeSimpleFieldValues() throws IOException { @Test @DisplayName("simple list fields must be recognised") void standardFieldTypes_mustExposeSimpleListFieldValues() throws IOException { - InferenceResponse response = loadInference("inference/standard_field_types.json"); + InferenceResponse response = loadInference("products/extraction/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -308,7 +338,7 @@ private void testObjectSubFieldSimpleString(String fieldName, SimpleField subFie @Test @DisplayName("object list fields must be recognised") void standardFieldTypes_mustExposeObjectListFieldValues() throws IOException { - InferenceResponse response = loadInference("inference/standard_field_types.json"); + InferenceResponse response = loadInference("products/extraction/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -358,7 +388,7 @@ void standardFieldTypes_mustExposeObjectListFieldValues() throws IOException { @Test @DisplayName("simple / object / list variants must be recognised") void standardFieldTypes_mustExposeObjectFieldValues() throws IOException { - InferenceResponse response = loadInference("inference/standard_field_types.json"); + InferenceResponse response = loadInference("products/extraction/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -393,7 +423,7 @@ void standardFieldTypes_mustExposeObjectFieldValues() throws IOException { @Test @DisplayName("allow getting fields using generics") void standardFieldTypes_getWithGenerics() throws IOException { - InferenceResponse response = loadInference("inference/standard_field_types.json"); + InferenceResponse response = loadInference("products/extraction/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); InferenceFields fields = inference.getResult().getFields(); @@ -429,7 +459,7 @@ void standardFieldTypes_getWithGenerics() throws IOException { @Test @DisplayName("confidence and locations must be usable") void standardFieldTypes_confidenceAndLocations() throws IOException { - InferenceResponse response = loadInference("inference/standard_field_types.json"); + InferenceResponse response = loadInference("products/extraction/standard_field_types.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -469,7 +499,7 @@ class RawTextTest { @Test @DisplayName("raw texts option must be parsed and exposed") void rawTexts_mustBeAccessible() throws IOException { - InferenceResponse response = loadInference("inference/raw_texts.json"); + InferenceResponse response = loadInference("products/extraction/raw_texts.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -504,7 +534,7 @@ class RagMetadataTest { @Test @DisplayName("RAG metadata when matched") void rag_mustBeFilled_whenMatched() throws IOException { - InferenceResponse response = loadInference("inference/rag_matched.json"); + InferenceResponse response = loadInference("products/extraction/rag_matched.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -516,7 +546,7 @@ void rag_mustBeFilled_whenMatched() throws IOException { @Test @DisplayName("RAG metadata when not matched") void rag_mustBeNull_whenNotMatched() throws IOException { - InferenceResponse response = loadInference("inference/rag_not_matched.json"); + InferenceResponse response = loadInference("products/extraction/rag_not_matched.json"); Inference inference = response.getInference(); assertNotNull(inference); @@ -532,8 +562,10 @@ class RstDisplay { @Test @DisplayName("rst display must be parsed and exposed") void rstDisplay_mustBeAccessible() throws IOException { - InferenceResponse resp = loadInference("inference/standard_field_types.json"); - String rstRef = readFileAsString(getV2ResourcePath("inference/standard_field_types.rst")); + InferenceResponse resp = loadInference("products/extraction/standard_field_types.json"); + String rstRef = readFileAsString( + getV2ResourcePath("products/extraction/standard_field_types.rst") + ); Inference inference = resp.getInference(); assertNotNull(inference); assertEquals(rstRef, resp.getInference().toString()); @@ -546,7 +578,7 @@ class TextContextTest { @Test @DisplayName("should be present and true when enabled") void textContext_mustBePresentAndTrue() throws IOException { - InferenceResponse resp = loadInference("inference/text_context_enabled.json"); + InferenceResponse resp = loadInference("products/extraction/text_context_enabled.json"); Inference inference = resp.getInference(); assertNotNull(inference); assertTrue(inference.getActiveOptions().getTextContext()); @@ -559,7 +591,7 @@ class DataSchemaTest { @Test @DisplayName("should be present and true when enabled") void textContext_mustBePresentAndTrue() throws IOException { - InferenceResponse resp = loadInference("inference/data_schema_replace.json"); + InferenceResponse resp = loadInference("products/extraction/data_schema_replace.json"); Inference inference = resp.getInference(); assertNotNull(inference); InferenceFields fields = inference.getResult().getFields(); diff --git a/src/test/resources b/src/test/resources index 0c51e1d3..37f2e3de 160000 --- a/src/test/resources +++ b/src/test/resources @@ -1 +1 @@ -Subproject commit 0c51e1d3e2258404c44280f25f4951ba6fe27324 +Subproject commit 37f2e3de48918e3b1a0e4604a9292aaeae05c637