From b39d3b7eb1ce4d08a9e1e21ff5e02d543c9e0c08 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:48:08 +0200 Subject: [PATCH 01/12] [DRAFT] add support for Mindee Client V2 --- src/main/java/com/mindee/MindeeClientV2.java | 166 ++++++++++++++++++ .../java/com/mindee/MindeeSettingsV2.java | 53 ++++++ .../java/com/mindee/PredictOptionsV2.java | 72 ++++++++ src/main/java/com/mindee/http/MindeeApi.java | 39 +--- .../java/com/mindee/http/MindeeApiCommon.java | 43 +++++ .../java/com/mindee/http/MindeeApiV2.java | 28 +++ .../java/com/mindee/http/MindeeHttpApiV2.java | 87 +++++++++ ...er.java => LocalDateTimeDeserializer.java} | 2 +- .../parsing/v2/AsyncInferenceResponse.java | 12 ++ .../mindee/parsing/v2/AsyncJobResponse.java | 21 +++ .../java/com/mindee/parsing/v2/BaseField.java | 8 + .../com/mindee/parsing/v2/CommonResponse.java | 23 +++ .../com/mindee/parsing/v2/DynamicField.java | 70 ++++++++ .../parsing/v2/DynamicFieldDeserializer.java | 44 +++++ .../com/mindee/parsing/v2/ErrorResponse.java | 36 ++++ .../java/com/mindee/parsing/v2/Inference.java | 56 ++++++ .../mindee/parsing/v2/InferenceFields.java | 14 ++ .../com/mindee/parsing/v2/InferenceFile.java | 30 ++++ .../com/mindee/parsing/v2/InferenceModel.java | 25 +++ .../mindee/parsing/v2/InferenceOptions.java | 4 + .../mindee/parsing/v2/InferenceResult.java | 42 +++++ src/main/java/com/mindee/parsing/v2/Job.java | 85 +++++++++ .../java/com/mindee/parsing/v2/ListField.java | 39 ++++ .../com/mindee/parsing/v2/ObjectField.java | 36 ++++ .../com/mindee/parsing/v2/SimpleField.java | 32 ++++ .../parsing/v2/SimpleFieldDeserializer.java | 34 ++++ .../java/com/mindee/parsing/v2/Webhook.java | 48 +++++ 27 files changed, 1111 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/mindee/MindeeClientV2.java create mode 100644 src/main/java/com/mindee/MindeeSettingsV2.java create mode 100644 src/main/java/com/mindee/PredictOptionsV2.java create mode 100644 src/main/java/com/mindee/http/MindeeApiCommon.java create mode 100644 src/main/java/com/mindee/http/MindeeApiV2.java create mode 100644 src/main/java/com/mindee/http/MindeeHttpApiV2.java rename src/main/java/com/mindee/parsing/common/{LocalDateTameTimeDeserializer.java => LocalDateTimeDeserializer.java} (94%) create mode 100644 src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java create mode 100644 src/main/java/com/mindee/parsing/v2/AsyncJobResponse.java create mode 100644 src/main/java/com/mindee/parsing/v2/BaseField.java create mode 100644 src/main/java/com/mindee/parsing/v2/CommonResponse.java create mode 100644 src/main/java/com/mindee/parsing/v2/DynamicField.java create mode 100644 src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java create mode 100644 src/main/java/com/mindee/parsing/v2/ErrorResponse.java create mode 100644 src/main/java/com/mindee/parsing/v2/Inference.java create mode 100644 src/main/java/com/mindee/parsing/v2/InferenceFields.java create mode 100644 src/main/java/com/mindee/parsing/v2/InferenceFile.java create mode 100644 src/main/java/com/mindee/parsing/v2/InferenceModel.java create mode 100644 src/main/java/com/mindee/parsing/v2/InferenceOptions.java create mode 100644 src/main/java/com/mindee/parsing/v2/InferenceResult.java create mode 100644 src/main/java/com/mindee/parsing/v2/Job.java create mode 100644 src/main/java/com/mindee/parsing/v2/ListField.java create mode 100644 src/main/java/com/mindee/parsing/v2/ObjectField.java create mode 100644 src/main/java/com/mindee/parsing/v2/SimpleField.java create mode 100644 src/main/java/com/mindee/parsing/v2/SimpleFieldDeserializer.java create mode 100644 src/main/java/com/mindee/parsing/v2/Webhook.java diff --git a/src/main/java/com/mindee/MindeeClientV2.java b/src/main/java/com/mindee/MindeeClientV2.java new file mode 100644 index 000000000..716414910 --- /dev/null +++ b/src/main/java/com/mindee/MindeeClientV2.java @@ -0,0 +1,166 @@ +package com.mindee; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mindee.http.MindeeApiV2; +import com.mindee.http.MindeeHttpApiV2; +import com.mindee.input.LocalInputSource; +import com.mindee.input.LocalResponse; +import com.mindee.input.PageOptions; +import com.mindee.pdf.PdfBoxApi; +import com.mindee.pdf.PdfOperation; +import com.mindee.pdf.SplitQuery; +import com.mindee.parsing.v2.AsyncInferenceResponse; +import com.mindee.parsing.v2.AsyncJobResponse; +import com.mindee.parsing.v2.PredictParameterV2; +import com.mindee.parsing.v2.InferenceOptionsV2; +import com.mindee.parsing.v2.AsyncPollingOptions; +import java.io.IOException; + +/** + * Entry point for the Mindee **V2** API features. + */ +public class MindeeClientV2 { + + private final PdfOperation pdfOperation; + private final MindeeApiV2 mindeeApi; + + /** Uses an API-key read from the environment variables. */ + public MindeeClientV2() { + this(new PdfBoxApi(), createDefaultApiV2("")); + } + + /** Uses the supplied API-key. */ + public MindeeClientV2(String apiKey) { + this(new PdfBoxApi(), createDefaultApiV2(apiKey)); + } + + /** Directly inject an already configured {@link MindeeApiV2}. */ + public MindeeClientV2(MindeeApiV2 mindeeApi) { + this(new PdfBoxApi(), mindeeApi); + } + + /** Inject both a PDF implementation and a HTTP implementation. */ + public MindeeClientV2(PdfOperation pdfOperation, MindeeApiV2 mindeeApi) { + this.pdfOperation = pdfOperation; + this.mindeeApi = mindeeApi; + } + + /* ------------------------------------------------------------------ */ + /* Queue helpers */ + /* ------------------------------------------------------------------ */ + + /** + * Enqueue a document in the asynchronous “Generated” queue. + */ + public AsyncJobResponse enqueue( + LocalInputSource inputSource, + InferenceOptionsV2 options, + PageOptions pageOptions) throws IOException { + + if (pageOptions != null && inputSource.isPdf()) { + inputSource.setFileBytes( + pdfOperation.split(new SplitQuery(inputSource.getFileBytes(), pageOptions)).getFile()); + } + + PredictParameterV2 payload = new PredictParameterV2( + inputSource, + options.getModelId(), + options.getAlias(), + options.getWebhookIds(), + options.getRag()); + + return mindeeApi.enqueuePost(payload); + } + + /** Overload without page options. */ + public AsyncJobResponse enqueue( + LocalInputSource inputSource, + InferenceOptionsV2 options) throws IOException { + return enqueue(inputSource, options, null); + } + + /** + * Retrieve results for a previously enqueued document. + */ + public AsyncInferenceResponse parseQueued(String jobId) { + if (jobId == null || jobId.isBlank()) { + throw new IllegalArgumentException("jobId must not be null or blank."); + } + return mindeeApi.getInferenceFromQueue(jobId); + } + + /** + * Convenience helper: enqueue, poll, and return the final inference. + */ + public AsyncInferenceResponse enqueueAndParse( + LocalInputSource inputSource, + InferenceOptionsV2 options, + PageOptions pageOptions, + AsyncPollingOptions polling) throws IOException, InterruptedException { + + if (polling == null) { + polling = new AsyncPollingOptions(); // default values + } + validatePollingOptions(polling); + + AsyncJobResponse job = enqueue(inputSource, options, pageOptions); + + Thread.sleep((long) (polling.getInitialDelaySec() * 1000)); + + int attempts = 0; + int max = polling.getMaxRetries(); + while (attempts < max) { + Thread.sleep((long) (polling.getIntervalSec() * 1000)); + AsyncInferenceResponse resp = parseQueued(job.getJob().getId()); + if (resp.getInference() != null) { + return resp; + } + attempts++; + } + throw new RuntimeException("Max retries exceeded (" + max + ")."); + } + + /** Overload with defaults (no page splitting, default polling). */ + public AsyncInferenceResponse enqueueAndParse( + LocalInputSource inputSource, + InferenceOptionsV2 options) throws IOException, InterruptedException { + return enqueueAndParse(inputSource, options, null, null); + } + + /* ------------------------------------------------------------------ */ + /* Utility / helpers */ + /* ------------------------------------------------------------------ */ + + /** + * Deserialize a webhook payload (or any saved response) into + * {@link AsyncInferenceResponse}. + */ + public AsyncInferenceResponse loadInference(LocalResponse localResponse) throws IOException { + ObjectMapper mapper = new ObjectMapper().findAndConfigureModules(); + AsyncInferenceResponse model = + mapper.readValue(localResponse.getFile(), AsyncInferenceResponse.class); + model.setRawResponse(localResponse.toString()); + return model; + } + + private static MindeeApiV2 createDefaultApiV2(String apiKey) { + MindeeSettings settings = apiKey == null || apiKey.isBlank() + ? new MindeeSettings() + : new MindeeSettings(apiKey); + return MindeeHttpApiV2.builder() + .mindeeSettings(settings) + .build(); + } + + private static void validatePollingOptions(AsyncPollingOptions p) { + if (p.getInitialDelaySec() < 1) { + throw new IllegalArgumentException("Initial delay must be ≥ 1 s"); + } + if (p.getIntervalSec() < 1) { + throw new IllegalArgumentException("Interval must be ≥ 1 s"); + } + if (p.getMaxRetries() < 2) { + throw new IllegalArgumentException("Max retries must be ≥ 2"); + } + } +} diff --git a/src/main/java/com/mindee/MindeeSettingsV2.java b/src/main/java/com/mindee/MindeeSettingsV2.java new file mode 100644 index 000000000..c87547fd9 --- /dev/null +++ b/src/main/java/com/mindee/MindeeSettingsV2.java @@ -0,0 +1,53 @@ +package com.mindee; + +import lombok.Getter; + +import java.util.Optional; + +/** + * Mindee API V2 configuration. + */ +@Getter +public class MindeeSettingsV2 { + + private static final String DEFAULT_MINDEE_V2_API_URL = "https://api-v2.mindee.net/v1"; + private final String apiKey; + private final String baseUrl; + + public MindeeSettingsV2() { + this("", ""); + } + + public Optional getApiKey() { + return Optional.ofNullable(apiKey); + } + + public MindeeSettingsV2(String apiKey) { + this(apiKey, ""); + } + + public MindeeSettingsV2(String apiKey, String baseUrl) { + + if (apiKey == null || apiKey.trim().isEmpty()) { + String apiKeyFromEnv = System.getenv("MINDEE_V2_API_KEY"); + if (apiKeyFromEnv == null || apiKeyFromEnv.trim().isEmpty()) { + this.apiKey = null; + } else { + this.apiKey = apiKeyFromEnv; + } + } else { + this.apiKey = apiKey; + } + + if (baseUrl == null || baseUrl.trim().isEmpty()) { + String baseUrlFromEnv = System.getenv("MINDEE_V2_API_URL"); + if (baseUrlFromEnv != null && !baseUrlFromEnv.trim().isEmpty()) { + this.baseUrl = baseUrlFromEnv; + } else { + this.baseUrl = DEFAULT_MINDEE_V2_API_URL; + } + } else { + this.baseUrl = baseUrl; + } + } +} diff --git a/src/main/java/com/mindee/PredictOptionsV2.java b/src/main/java/com/mindee/PredictOptionsV2.java new file mode 100644 index 000000000..69af2e7ce --- /dev/null +++ b/src/main/java/com/mindee/PredictOptionsV2.java @@ -0,0 +1,72 @@ +package com.mindee; + +import com.mindee.input.LocalInputSource; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Parameters for the V2 “predict” endpoint. + * + *

This is a pure config object – no Jackson annotations are used.

+ */ +@Getter +public final class PredictOptionsV2 { + + /** + * Optional alias for the file. + */ + private final String alias; + + /** + * ID of the model. + */ + private final String modelId; + + /** + * If {@code true}, enable Retrieval-Augmented Generation. + */ + private final boolean rag; + + /** + * IDs of webhooks to propagate the API response to (may be empty). + */ + private final List webhookIds; + + /** + * Local input source. + */ + private final LocalInputSource localSource; + + public PredictOptionsV2( + LocalInputSource localSource, + String modelId, + boolean rag, + String alias, + List webhookIds + ) { + + this.localSource = Objects.requireNonNull(localSource, "localSource must not be null"); + this.modelId = Objects.requireNonNull(modelId, "modelId must not be null"); + this.rag = rag; + this.alias = alias; + this.webhookIds = webhookIds == null ? Collections.emptyList() + : webhookIds; + } + + @Builder(builderMethodName = "builder") + private static PredictOptionsV2 build( + LocalInputSource localSource, + String modelId, + boolean rag, + String alias, + @Singular List webhookIds + ) { + + return new PredictOptionsV2(localSource, modelId, rag, alias, webhookIds); + } +} diff --git a/src/main/java/com/mindee/http/MindeeApi.java b/src/main/java/com/mindee/http/MindeeApi.java index 051d1ffc5..efd4243a8 100644 --- a/src/main/java/com/mindee/http/MindeeApi.java +++ b/src/main/java/com/mindee/http/MindeeApi.java @@ -4,14 +4,13 @@ import com.mindee.parsing.common.Inference; import com.mindee.parsing.common.PredictResponse; import com.mindee.parsing.common.WorkflowResponse; -import java.io.ByteArrayOutputStream; + import java.io.IOException; -import org.apache.hc.core5.http.HttpEntity; /** * Defines required methods for an API. */ -abstract public class MindeeApi { +abstract public class MindeeApi extends MindeeApiCommon { /** * Get a document from the predict queue. @@ -45,38 +44,4 @@ abstract public WorkflowResponse executeWorkflowP String workflowId, RequestParameters requestParameters ) throws IOException; - - protected String getUserAgent() { - String javaVersion = System.getProperty("java.version"); - String sdkVersion = getClass().getPackage().getImplementationVersion(); - String osName = System.getProperty("os.name").toLowerCase(); - - if (osName.contains("windows")) { - osName = "windows"; - } else if (osName.contains("darwin")) { - osName = "macos"; - } else if (osName.contains("mac")) { - osName = "macos"; - } else if (osName.contains("linux")) { - osName = "linux"; - } else if (osName.contains("bsd")) { - osName = "bsd"; - } else if (osName.contains("aix")) { - osName = "aix"; - } - return String.format("mindee-api-java@v%s java-v%s %s", sdkVersion, javaVersion, osName); - } - - protected boolean is2xxStatusCode(int statusCode) { - return statusCode >= 200 && statusCode <= 299; - } - - protected String readRawResponse(HttpEntity responseEntity) throws IOException { - ByteArrayOutputStream contentRead = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - for (int length; (length = responseEntity.getContent().read(buffer)) != -1; ) { - contentRead.write(buffer, 0, length); - } - return contentRead.toString("UTF-8"); - } } diff --git a/src/main/java/com/mindee/http/MindeeApiCommon.java b/src/main/java/com/mindee/http/MindeeApiCommon.java new file mode 100644 index 000000000..dae787fb7 --- /dev/null +++ b/src/main/java/com/mindee/http/MindeeApiCommon.java @@ -0,0 +1,43 @@ +package com.mindee.http; + +import org.apache.hc.core5.http.HttpEntity; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public abstract class MindeeApiCommon { + + protected String getUserAgent() { + String javaVersion = System.getProperty("java.version"); + String sdkVersion = getClass().getPackage().getImplementationVersion(); + String osName = System.getProperty("os.name").toLowerCase(); + + if (osName.contains("windows")) { + osName = "windows"; + } else if (osName.contains("darwin")) { + osName = "macos"; + } else if (osName.contains("mac")) { + osName = "macos"; + } else if (osName.contains("linux")) { + osName = "linux"; + } else if (osName.contains("bsd")) { + osName = "bsd"; + } else if (osName.contains("aix")) { + osName = "aix"; + } + return String.format("mindee-api-java@v%s java-v%s %s", sdkVersion, javaVersion, osName); + } + + protected boolean is2xxStatusCode(int statusCode) { + return statusCode >= 200 && statusCode <= 299; + } + + protected String readRawResponse(HttpEntity responseEntity) throws IOException { + ByteArrayOutputStream contentRead = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int length; (length = responseEntity.getContent().read(buffer)) != -1; ) { + contentRead.write(buffer, 0, length); + } + return contentRead.toString("UTF-8"); + } +} diff --git a/src/main/java/com/mindee/http/MindeeApiV2.java b/src/main/java/com/mindee/http/MindeeApiV2.java new file mode 100644 index 000000000..8513b7351 --- /dev/null +++ b/src/main/java/com/mindee/http/MindeeApiV2.java @@ -0,0 +1,28 @@ +package com.mindee.http; + +import com.mindee.PredictOptionsV2; +import com.mindee.parsing.v2.AsyncInferenceResponse; +import com.mindee.parsing.v2.AsyncJobResponse; + +import java.io.IOException; + +/** + * Defines required methods for an API. + */ +abstract public class MindeeApiV2 extends MindeeApiCommon { + /** + * Send a file to the prediction queue. + */ + abstract public AsyncJobResponse enqueuePost( + PredictOptionsV2 options, + RequestParameters requestParameters + ) throws IOException; + + /** + * Get a document from the predict queue. + */ + abstract public AsyncInferenceResponse getInferenceFromQueue( + String jobId + ); + +} diff --git a/src/main/java/com/mindee/http/MindeeHttpApiV2.java b/src/main/java/com/mindee/http/MindeeHttpApiV2.java new file mode 100644 index 000000000..4aaaf472d --- /dev/null +++ b/src/main/java/com/mindee/http/MindeeHttpApiV2.java @@ -0,0 +1,87 @@ +package com.mindee.http; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mindee.MindeeException; +import com.mindee.MindeeSettingsV2; +import com.mindee.PredictOptionsV2; +import com.mindee.parsing.common.*; +import com.mindee.parsing.v2.AsyncInferenceResponse; +import com.mindee.parsing.v2.AsyncJobResponse; +import com.mindee.parsing.v2.CommonResponse; +import lombok.Builder; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.net.URIBuilder; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * HTTP Client class for the V2 API. + */ +public final class MindeeHttpApiV2 extends MindeeApiV2 { + + private static final ObjectMapper mapper = new ObjectMapper(); + private final Function buildProductPredicBasetUrl = this::buildProductPredictBaseUrl; + private final Function buildWorkflowPredictBaseUrl = this::buildWorkflowPredictBaseUrl; + private final Function buildWorkflowExecutionBaseUrl = this::buildWorkflowExecutionUrl; + /** + * The MindeeSetting needed to make the api call. + */ + private final MindeeSettingsV2 mindeeSettings; + /** + * The HttpClientBuilder used to create HttpClient objects used to make api calls over http. + * Defaults to HttpClientBuilder.create().useSystemProperties() + */ + private final HttpClientBuilder httpClientBuilder; + /** + * The function used to generate the synchronous API endpoint URL. + * Only needs to be set if the api calls need to be directed through internal URLs. + */ + private final Function enqueuePost; + private final Function urlFromEndpoint; + + public MindeeHttpApiV2(MindeeSettingsV2 mindeeSettings) { + this( + mindeeSettings, + null, + null, + null, + null, + null, + null + ); + } + + @Builder + private MindeeHttpApiV2( + MindeeSettingsV2 mindeeSettings, + HttpClientBuilder httpClientBuilder, + Function enqueuePost, + Function getInferenceFromQueue + ) { + this.mindeeSettings = mindeeSettings; + + if (httpClientBuilder != null) { + this.httpClientBuilder = httpClientBuilder; + } else { + this.httpClientBuilder = HttpClientBuilder.create().useSystemProperties(); + } + // TODO + } +} diff --git a/src/main/java/com/mindee/parsing/common/LocalDateTameTimeDeserializer.java b/src/main/java/com/mindee/parsing/common/LocalDateTimeDeserializer.java similarity index 94% rename from src/main/java/com/mindee/parsing/common/LocalDateTameTimeDeserializer.java rename to src/main/java/com/mindee/parsing/common/LocalDateTimeDeserializer.java index e5f8de4bf..3aadcd05c 100644 --- a/src/main/java/com/mindee/parsing/common/LocalDateTameTimeDeserializer.java +++ b/src/main/java/com/mindee/parsing/common/LocalDateTimeDeserializer.java @@ -14,7 +14,7 @@ /** * Deserializer for LocalDateTime */ -class LocalDateTimeDeserializer extends JsonDeserializer { +public class LocalDateTimeDeserializer extends JsonDeserializer { @Override public LocalDateTime deserialize( JsonParser jsonParser, diff --git a/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java b/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java new file mode 100644 index 000000000..18f35e64f --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java @@ -0,0 +1,12 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AsyncInferenceResponse extends CommonResponse { + + /** + * Inference result. + */ + @JsonProperty("inference") + private Inference inference; +} diff --git a/src/main/java/com/mindee/parsing/v2/AsyncJobResponse.java b/src/main/java/com/mindee/parsing/v2/AsyncJobResponse.java new file mode 100644 index 000000000..f2d1407cc --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/AsyncJobResponse.java @@ -0,0 +1,21 @@ +package com.mindee.parsing.v2; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Represents an asynchronous polling response. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AsyncJobResponse extends CommonResponse { + /** + * Representation of the Job. + */ + @JsonProperty("job") + Job job; +} diff --git a/src/main/java/com/mindee/parsing/v2/BaseField.java b/src/main/java/com/mindee/parsing/v2/BaseField.java new file mode 100644 index 000000000..fc2081d24 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/BaseField.java @@ -0,0 +1,8 @@ +package com.mindee.parsing.v2; + +/** + * Base class for V2 fields. + */ +public abstract class BaseField { + +} diff --git a/src/main/java/com/mindee/parsing/v2/CommonResponse.java b/src/main/java/com/mindee/parsing/v2/CommonResponse.java new file mode 100644 index 000000000..263b31c2d --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/CommonResponse.java @@ -0,0 +1,23 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Common response information from Mindee API V2. + */ +@Data +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +abstract public class CommonResponse { + /** + * The raw server response. + * This is not formatted in any way by the library and may contain newline and tab characters. + */ + private String rawResponse; + + public void setRawResponse(String contents) { + rawResponse = contents; + } +} diff --git a/src/main/java/com/mindee/parsing/v2/DynamicField.java b/src/main/java/com/mindee/parsing/v2/DynamicField.java new file mode 100644 index 000000000..1e26b0fc8 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/DynamicField.java @@ -0,0 +1,70 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Dynamically-typed field (simple / object / list). + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = DynamicFieldDeserializer.class) +@AllArgsConstructor +@NoArgsConstructor +public class DynamicField { + + /** + * Type of the wrapped field. + */ + @JsonProperty("type") + private FieldType type; + + /** + * Value as simple field. + */ + @JsonProperty("simple_field") + private SimpleField simpleField; + + /** + * Value as list field. + */ + @JsonProperty("list_field") + private ListField listField; + + /** + * Value as object field. + */ + @JsonProperty("object_field") + private ObjectField objectField; + + public static DynamicField of(SimpleField value) { + return new DynamicField(FieldType.SIMPLE_FIELD, value, null, null); + } + + public static DynamicField of(ObjectField value) { + return new DynamicField(FieldType.OBJECT_FIELD, null, null, value); + } + + public static DynamicField of(ListField value) { + return new DynamicField(FieldType.LIST_FIELD, null, value, null); + } + + @Override + public String toString() { + if (simpleField != null) return simpleField.toString(); + if (listField != null) return listField.toString(); + if (objectField != null) return objectField.toString(); + return ""; + } + + /** + * Possible field kinds. + */ + public enum FieldType { SIMPLE_FIELD, OBJECT_FIELD, LIST_FIELD } +} diff --git a/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java b/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java new file mode 100644 index 000000000..31090785a --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java @@ -0,0 +1,44 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.IOException; + +/** + * Custom deserializer for {@link DynamicField}. + */ +public final class DynamicFieldDeserializer extends JsonDeserializer { + + @Override + public DynamicField deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectCodec codec = jp.getCodec(); + JsonNode root = codec.readTree(jp); + + // -------- LIST FEATURE -------- + if (root.has("items") && root.get("items").isArray()) { + ListField list = new ListField(); + for (JsonNode itemNode : root.get("items")) { + list.getItems().add(codec.treeToValue(itemNode, DynamicField.class)); + } + return DynamicField.of(list); + } + + // -------- OBJECT WITH NESTED FIELDS -------- + if (root.has("fields") && root.get("fields").isObject()) { + ObjectField objectField = codec.treeToValue(root, ObjectField.class); + return DynamicField.of(objectField); + } + + // -------- SIMPLE OBJECT -------- + if (root.has("value")) { + SimpleField simple = codec.treeToValue(root, SimpleField.class); + return DynamicField.of(simple); + } + + return null; + } +} diff --git a/src/main/java/com/mindee/parsing/v2/ErrorResponse.java b/src/main/java/com/mindee/parsing/v2/ErrorResponse.java new file mode 100644 index 000000000..990b07f0b --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/ErrorResponse.java @@ -0,0 +1,36 @@ +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 information from the API. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public final class ErrorResponse { + /** + * Detail relevant to the error. + */ + @JsonProperty("detail") + private String detail; + + /** + * HTTP error code. + */ + @JsonProperty("status") + private int status; + + /** For prettier display. */ + @Override + public String toString() { + return "HTTP Status: " + status + " - " + detail; + } +} diff --git a/src/main/java/com/mindee/parsing/v2/Inference.java b/src/main/java/com/mindee/parsing/v2/Inference.java new file mode 100644 index 000000000..a23bcbf32 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/Inference.java @@ -0,0 +1,56 @@ +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; + +import java.util.StringJoiner; + +/** + * Inference object for the V2 API. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class Inference { + + /** + * Model info. + */ + @JsonProperty("model") + private InferenceModel model; + + /** + * File info. + */ + @JsonProperty("file") + private InferenceFile file; + + /** + * Model result values. + */ + @JsonProperty("result") + private InferenceResult result; + + @Override + public String toString() { + StringJoiner sj = new StringJoiner("\n"); + sj.add("#########") + .add("Inference") + .add("#########") + .add(":Model: " + (model != null ? model.getId() : "")) + .add(":File:") + .add(" :Name: " + (file != null ? file.getName() : "")) + .add(" :Alias: " + (file != null ? file.getAlias() : "")) + .add("") + .add("Result") + .add("======") + .add(result != null ? result.toString() : ""); + return sj.toString().trim(); + } +} diff --git a/src/main/java/com/mindee/parsing/v2/InferenceFields.java b/src/main/java/com/mindee/parsing/v2/InferenceFields.java new file mode 100644 index 000000000..1670494b1 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/InferenceFields.java @@ -0,0 +1,14 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.EqualsAndHashCode; + +import java.util.HashMap; + +/** + * Inference fields map. + */ +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class InferenceFields extends HashMap { +} diff --git a/src/main/java/com/mindee/parsing/v2/InferenceFile.java b/src/main/java/com/mindee/parsing/v2/InferenceFile.java new file mode 100644 index 000000000..3f7c8d8a1 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/InferenceFile.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; + +/** + * File info for V2 API. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class InferenceFile { + /** + * File name. + */ + @JsonProperty("name") + private String name; + + /** + * Optional file alias. + */ + @JsonProperty("alias") + private String alias; +} diff --git a/src/main/java/com/mindee/parsing/v2/InferenceModel.java b/src/main/java/com/mindee/parsing/v2/InferenceModel.java new file mode 100644 index 000000000..02c24f74f --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/InferenceModel.java @@ -0,0 +1,25 @@ +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; + +/** + * Model information for a V2 API inference. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class InferenceModel { + + /** + * The ID of the model. + */ + @JsonProperty("id") + private String id; +} diff --git a/src/main/java/com/mindee/parsing/v2/InferenceOptions.java b/src/main/java/com/mindee/parsing/v2/InferenceOptions.java new file mode 100644 index 000000000..1c675b0c4 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/InferenceOptions.java @@ -0,0 +1,4 @@ +package com.mindee.parsing.v2; + +public final class InferenceOptions { +} diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResult.java b/src/main/java/com/mindee/parsing/v2/InferenceResult.java new file mode 100644 index 000000000..ef0521283 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/InferenceResult.java @@ -0,0 +1,42 @@ +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; + +import java.util.Map; +import java.util.StringJoiner; + +/** + * Generic result for any off-the-shelf Mindee V2 model. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public final class InferenceResult { + + /** Model fields. */ + @JsonProperty("fields") + private InferenceFields fields; + + /** Options. */ + @JsonProperty("options") + private InferenceOptions options; + + @Override + public String toString() { + if (fields == null || fields.isEmpty()) { + return ""; + } + StringJoiner joiner = new StringJoiner("\n"); + for (Map.Entry e : fields.entrySet()) { + joiner.add(":" + e.getKey() + ": " + e.getValue()); + } + return joiner.toString(); + } +} diff --git a/src/main/java/com/mindee/parsing/v2/Job.java b/src/main/java/com/mindee/parsing/v2/Job.java new file mode 100644 index 000000000..f35841ea8 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/Job.java @@ -0,0 +1,85 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.mindee.parsing.common.LocalDateTimeDeserializer; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Defines an enqueued Job. + */ + +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public final class Job { + /** + * Date and time the job was created at. + */ + @JsonProperty("created_at") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private LocalDateTime createdAt; + + /** + * ID of the job. + */ + @JsonProperty("id") + private String id; + + /** + * Status of the job. + */ + @JsonProperty("status") + private String status; + + /** + * Status of the job. + */ + @JsonProperty("error") + private ErrorResponse error; + + /** + * ID of the model. + */ + @JsonProperty("model_id") + private String modelId; + + /** + * Name of the file. + */ + @JsonProperty("file_name") + private String fileName; + + /** + * Optional alias of the file. + */ + @JsonProperty("file_name") + private String fileAlias; + + /** + * Polling URL. + */ + @JsonProperty("polling_url") + private String pollingUrl; + + /** + * Result URL, when available. + */ + @JsonProperty("result_url") + private String resultUrl; + + /** + * Polling URL. + */ + @JsonProperty("webhooks") + private List webhooks; +} diff --git a/src/main/java/com/mindee/parsing/v2/ListField.java b/src/main/java/com/mindee/parsing/v2/ListField.java new file mode 100644 index 000000000..3c7c080e3 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/ListField.java @@ -0,0 +1,39 @@ +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; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +/** + * Field holding a list of fields. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public final class ListField extends BaseField { + + /** + * Items of the list. + */ + @JsonProperty("items") + private List items = new ArrayList<>(); + + @Override + public String toString() { + if (items == null || items.isEmpty()) { + return ""; + } + StringJoiner joiner = new StringJoiner("\n"); + items.forEach(f -> joiner.add(f == null ? "null" : f.toString())); + return joiner.toString(); + } +} diff --git a/src/main/java/com/mindee/parsing/v2/ObjectField.java b/src/main/java/com/mindee/parsing/v2/ObjectField.java new file mode 100644 index 000000000..3e057cbdd --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/ObjectField.java @@ -0,0 +1,36 @@ +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; + +import java.util.Map; +import java.util.StringJoiner; + +/** + * Field holding a map of sub-fields. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class ObjectField extends BaseField { + + /** + * Sub-fields keyed by their name. + */ + @JsonProperty("fields") + private Map fields; + + @Override + public String toString() { + if (fields == null || fields.isEmpty()) return ""; + StringJoiner joiner = new StringJoiner("\n"); + fields.forEach((k, v) -> joiner.add(k + ": " + v)); + return joiner.toString(); + } +} diff --git a/src/main/java/com/mindee/parsing/v2/SimpleField.java b/src/main/java/com/mindee/parsing/v2/SimpleField.java new file mode 100644 index 000000000..03f2ed1a9 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/SimpleField.java @@ -0,0 +1,32 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Field holding a single scalar value. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = SimpleFieldDeserializer.class) +@AllArgsConstructor +@NoArgsConstructor +public final class SimpleField extends BaseField { + + /** + * Value (string, boolean, number … or {@code null}). + */ + @JsonProperty("value") + private Object value; + + @Override + public String toString() { + return value == null ? "null" : value.toString(); + } +} diff --git a/src/main/java/com/mindee/parsing/v2/SimpleFieldDeserializer.java b/src/main/java/com/mindee/parsing/v2/SimpleFieldDeserializer.java new file mode 100644 index 000000000..049189db0 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/SimpleFieldDeserializer.java @@ -0,0 +1,34 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; + +/** + * Custom deserializer for {@link SimpleField}. + */ +public final class SimpleFieldDeserializer extends JsonDeserializer { + + @Override + public SimpleField deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectCodec codec = jp.getCodec(); + JsonNode root = codec.readTree(jp); + + JsonNode valueNode = root.get("value"); + Object value = null; + + if (valueNode != null && !valueNode.isNull()) { + if (valueNode.isTextual()) { + value = valueNode.asText(); + } else if (valueNode.isNumber()) { + value = valueNode.doubleValue(); + } else if (valueNode.isBoolean()) { + value = valueNode.asBoolean(); + } + } + return new SimpleField(value); + } +} diff --git a/src/main/java/com/mindee/parsing/v2/Webhook.java b/src/main/java/com/mindee/parsing/v2/Webhook.java new file mode 100644 index 000000000..6fd5aa613 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/Webhook.java @@ -0,0 +1,48 @@ +package com.mindee.parsing.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.mindee.parsing.common.LocalDateTimeDeserializer; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * Webhook info. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public final class Webhook { + + /** + * ID of the webhook. + */ + @JsonProperty("id") + private String id; + + /** + * An error encountered while processing the webhook. + */ + @JsonProperty("error") + private ErrorResponse error; + + /** + * Date and time the webhook was created at. + */ + @JsonProperty("created_at") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private LocalDateTime createdAt; + + /** + * Status of the webhook. + */ + @JsonProperty("status") + private String status; +} From c41ecb37cf9645520597eb0dbe44d8859aa63d03 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:22:30 +0200 Subject: [PATCH 02/12] :sparkles: add support for V2 client --- src/main/java/com/mindee/CommonClient.java | 36 +++ .../com/mindee/InferencePredictOptions.java | 123 ++++++++++ src/main/java/com/mindee/MindeeClient.java | 19 +- src/main/java/com/mindee/MindeeClientV2.java | 83 ++----- .../java/com/mindee/MindeeSettingsV2.java | 3 +- .../java/com/mindee/PredictOptionsV2.java | 72 ------ src/main/java/com/mindee/http/MindeeApi.java | 1 - .../java/com/mindee/http/MindeeApiCommon.java | 16 +- .../java/com/mindee/http/MindeeApiV2.java | 12 +- .../java/com/mindee/http/MindeeHttpApiV2.java | 222 +++++++++++++++--- .../mindee/http/MindeeHttpExceptionV2.java | 29 +++ .../java/com/mindee/input/PageOptions.java | 27 +++ .../parsing/v2/AsyncInferenceResponse.java | 3 + .../parsing/v2/DynamicFieldDeserializer.java | 1 - .../java/com/mindee/parsing/v2/Inference.java | 3 +- .../mindee/parsing/v2/InferenceFields.java | 3 +- .../mindee/parsing/v2/InferenceOptions.java | 11 + .../mindee/parsing/v2/InferenceResult.java | 5 +- src/main/java/com/mindee/parsing/v2/Job.java | 5 +- .../java/com/mindee/parsing/v2/ListField.java | 7 +- .../com/mindee/parsing/v2/ObjectField.java | 5 +- .../java/com/mindee/parsing/v2/Webhook.java | 3 +- 22 files changed, 474 insertions(+), 215 deletions(-) create mode 100644 src/main/java/com/mindee/CommonClient.java create mode 100644 src/main/java/com/mindee/InferencePredictOptions.java delete mode 100644 src/main/java/com/mindee/PredictOptionsV2.java create mode 100644 src/main/java/com/mindee/http/MindeeHttpExceptionV2.java diff --git a/src/main/java/com/mindee/CommonClient.java b/src/main/java/com/mindee/CommonClient.java new file mode 100644 index 000000000..b7d45f95e --- /dev/null +++ b/src/main/java/com/mindee/CommonClient.java @@ -0,0 +1,36 @@ +package com.mindee; + +import com.mindee.input.LocalInputSource; +import com.mindee.input.PageOptions; +import com.mindee.pdf.PdfOperation; +import com.mindee.pdf.SplitQuery; +import java.io.IOException; + +/** + * Common client for all Mindee API clients. + */ +public abstract class CommonClient { + protected PdfOperation pdfOperation; + + /** + * Retrieves the file after applying page operations to it. + * @param localInputSource Local input source to apply operations to. + * @param pageOptions Options to apply. + * @return A byte array of the file after applying page operations. + * @throws IOException Throws if the file can't be accessed. + */ + protected byte[] getSplitFile( + LocalInputSource localInputSource, + PageOptions pageOptions + ) throws IOException { + byte[] splitFile; + if (pageOptions == null || !localInputSource.isPdf()) { + splitFile = localInputSource.getFile(); + } else { + splitFile = pdfOperation.split( + new SplitQuery(localInputSource.getFile(), pageOptions) + ).getFile(); + } + return splitFile; + } +} diff --git a/src/main/java/com/mindee/InferencePredictOptions.java b/src/main/java/com/mindee/InferencePredictOptions.java new file mode 100644 index 000000000..5a0371498 --- /dev/null +++ b/src/main/java/com/mindee/InferencePredictOptions.java @@ -0,0 +1,123 @@ +package com.mindee; + +import com.mindee.input.PageOptions; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import lombok.Data; +import lombok.Getter; + +/** + * Options to pass when calling methods using the API V2. + */ +@Getter +@Data +public final class InferencePredictOptions { + + /** + * ID of the model (required). + */ + private final String modelId; + /** + * Whether to include full text data for async APIs. + * Performing a full OCR on the server increases response time and payload size. + */ + private final boolean fullText; + /** + * Enables Retrieval-Augmented Generation (optional, default: {@code false}). + */ + private final boolean rag; + /** + * Optional alias for the file. + */ + private final String alias; + /** + * IDs of webhooks to propagate the API response to (may be empty). + */ + private final List webhookIds; + /** + * Page options to apply to the document. + */ + private final PageOptions pageOptions; + /* + * Asynchronous polling options. + */ + private final AsyncPollingOptions pollingOptions; + + /** + * Create a new builder. + * + * @param modelId the mandatory model identifier + * @return a fresh {@link Builder} + */ + public static Builder builder(String modelId) { + return new Builder(modelId); + } + + /** + * Fluent builder for {@link InferencePredictOptions}. + */ + public static final class Builder { + + private final String modelId; + private boolean fullText = false; + private boolean rag = false; + private String alias; + private List webhookIds = Collections.emptyList(); + private PageOptions pageOptions = null; + private AsyncPollingOptions pollingOptions = null; + + private Builder(String modelId) { + this.modelId = Objects.requireNonNull(modelId, "modelId must not be null"); + } + + /** + * Toggle full-text OCR extraction. + */ + public Builder fullText(boolean fullText) { + this.fullText = fullText; + return this; + } + + /** Enable / disable Retrieval-Augmented Generation. */ + public Builder rag(boolean rag) { + this.rag = rag; + return this; + } + + /** Set an alias for the uploaded document. */ + public Builder alias(String alias) { + this.alias = alias; + return this; + } + + /** Provide IDs of webhooks to forward the API response to. */ + public Builder webhookIds(List webhookIds) { + this.webhookIds = webhookIds; + return this; + } + + public Builder pageOptions(PageOptions pageOptions) { + this.pageOptions = pageOptions; + return this; + } + + public Builder pollingOptions(AsyncPollingOptions pollingOptions) { + this.pollingOptions = pollingOptions; + return this; + } + + /** Build an immutable {@link InferencePredictOptions} instance. */ + public InferencePredictOptions build() { + return new InferencePredictOptions( + modelId, + fullText, + rag, + alias, + webhookIds, + pageOptions, + pollingOptions + ); + } + } +} diff --git a/src/main/java/com/mindee/MindeeClient.java b/src/main/java/com/mindee/MindeeClient.java index 272d2195b..2558bd416 100644 --- a/src/main/java/com/mindee/MindeeClient.java +++ b/src/main/java/com/mindee/MindeeClient.java @@ -16,7 +16,6 @@ import com.mindee.parsing.common.WorkflowResponse; import com.mindee.pdf.PdfBoxApi; import com.mindee.pdf.PdfOperation; -import com.mindee.pdf.SplitQuery; import com.mindee.product.custom.CustomV1; import com.mindee.product.generated.GeneratedV1; import java.io.IOException; @@ -25,10 +24,9 @@ /** * Main entrypoint for Mindee operations. */ -public class MindeeClient { +public class MindeeClient extends CommonClient { private final MindeeApi mindeeApi; - private final PdfOperation pdfOperation; /** * Create a default MindeeClient. @@ -1124,19 +1122,4 @@ public AsyncPredictResponse loadPrediction( return objectMapper.readValue(localResponse.getFile(), parametricType); } - private byte[] getSplitFile( - LocalInputSource localInputSource, - PageOptions pageOptions - ) throws IOException { - byte[] splitFile; - if (pageOptions == null || !localInputSource.isPdf()) { - splitFile = localInputSource.getFile(); - } else { - splitFile = pdfOperation.split( - new SplitQuery(localInputSource.getFile(), pageOptions) - ).getFile(); - } - return splitFile; - } - } diff --git a/src/main/java/com/mindee/MindeeClientV2.java b/src/main/java/com/mindee/MindeeClientV2.java index 716414910..f4624e610 100644 --- a/src/main/java/com/mindee/MindeeClientV2.java +++ b/src/main/java/com/mindee/MindeeClientV2.java @@ -5,23 +5,17 @@ import com.mindee.http.MindeeHttpApiV2; import com.mindee.input.LocalInputSource; import com.mindee.input.LocalResponse; -import com.mindee.input.PageOptions; -import com.mindee.pdf.PdfBoxApi; -import com.mindee.pdf.PdfOperation; -import com.mindee.pdf.SplitQuery; import com.mindee.parsing.v2.AsyncInferenceResponse; import com.mindee.parsing.v2.AsyncJobResponse; -import com.mindee.parsing.v2.PredictParameterV2; -import com.mindee.parsing.v2.InferenceOptionsV2; -import com.mindee.parsing.v2.AsyncPollingOptions; +import com.mindee.parsing.v2.CommonResponse; +import com.mindee.pdf.PdfBoxApi; +import com.mindee.pdf.PdfOperation; import java.io.IOException; /** * Entry point for the Mindee **V2** API features. */ -public class MindeeClientV2 { - - private final PdfOperation pdfOperation; +public class MindeeClientV2 extends CommonClient { private final MindeeApiV2 mindeeApi; /** Uses an API-key read from the environment variables. */ @@ -45,45 +39,26 @@ public MindeeClientV2(PdfOperation pdfOperation, MindeeApiV2 mindeeApi) { this.mindeeApi = mindeeApi; } - /* ------------------------------------------------------------------ */ - /* Queue helpers */ - /* ------------------------------------------------------------------ */ - /** * Enqueue a document in the asynchronous “Generated” queue. */ public AsyncJobResponse enqueue( LocalInputSource inputSource, - InferenceOptionsV2 options, - PageOptions pageOptions) throws IOException { - - if (pageOptions != null && inputSource.isPdf()) { - inputSource.setFileBytes( - pdfOperation.split(new SplitQuery(inputSource.getFileBytes(), pageOptions)).getFile()); + InferencePredictOptions options) throws IOException { + LocalInputSource finalInput; + if (options.getPageOptions() != null) { + finalInput = new LocalInputSource(getSplitFile(inputSource, options.getPageOptions()), inputSource.getFilename()); + } else { + finalInput = inputSource; } - - PredictParameterV2 payload = new PredictParameterV2( - inputSource, - options.getModelId(), - options.getAlias(), - options.getWebhookIds(), - options.getRag()); - - return mindeeApi.enqueuePost(payload); - } - - /** Overload without page options. */ - public AsyncJobResponse enqueue( - LocalInputSource inputSource, - InferenceOptionsV2 options) throws IOException { - return enqueue(inputSource, options, null); + return mindeeApi.enqueuePost(finalInput, options); } /** * Retrieve results for a previously enqueued document. */ - public AsyncInferenceResponse parseQueued(String jobId) { - if (jobId == null || jobId.isBlank()) { + public CommonResponse parseQueued(String jobId) { + if (jobId == null || jobId.trim().isEmpty()) { throw new IllegalArgumentException("jobId must not be null or blank."); } return mindeeApi.getInferenceFromQueue(jobId); @@ -94,16 +69,15 @@ public AsyncInferenceResponse parseQueued(String jobId) { */ public AsyncInferenceResponse enqueueAndParse( LocalInputSource inputSource, - InferenceOptionsV2 options, - PageOptions pageOptions, + InferencePredictOptions options, AsyncPollingOptions polling) throws IOException, InterruptedException { if (polling == null) { - polling = new AsyncPollingOptions(); // default values + polling = AsyncPollingOptions.builder().build(); // default values } validatePollingOptions(polling); - AsyncJobResponse job = enqueue(inputSource, options, pageOptions); + AsyncJobResponse job = enqueue(inputSource, options); Thread.sleep((long) (polling.getInitialDelaySec() * 1000)); @@ -111,32 +85,21 @@ public AsyncInferenceResponse enqueueAndParse( int max = polling.getMaxRetries(); while (attempts < max) { Thread.sleep((long) (polling.getIntervalSec() * 1000)); - AsyncInferenceResponse resp = parseQueued(job.getJob().getId()); - if (resp.getInference() != null) { - return resp; + CommonResponse resp = parseQueued(job.getJob().getId()); + if (resp instanceof AsyncInferenceResponse) { + return (AsyncInferenceResponse) resp; } attempts++; } throw new RuntimeException("Max retries exceeded (" + max + ")."); } - /** Overload with defaults (no page splitting, default polling). */ - public AsyncInferenceResponse enqueueAndParse( - LocalInputSource inputSource, - InferenceOptionsV2 options) throws IOException, InterruptedException { - return enqueueAndParse(inputSource, options, null, null); - } - - /* ------------------------------------------------------------------ */ - /* Utility / helpers */ - /* ------------------------------------------------------------------ */ - /** * Deserialize a webhook payload (or any saved response) into * {@link AsyncInferenceResponse}. */ public AsyncInferenceResponse loadInference(LocalResponse localResponse) throws IOException { - ObjectMapper mapper = new ObjectMapper().findAndConfigureModules(); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); AsyncInferenceResponse model = mapper.readValue(localResponse.getFile(), AsyncInferenceResponse.class); model.setRawResponse(localResponse.toString()); @@ -144,9 +107,9 @@ public AsyncInferenceResponse loadInference(LocalResponse localResponse) throws } private static MindeeApiV2 createDefaultApiV2(String apiKey) { - MindeeSettings settings = apiKey == null || apiKey.isBlank() - ? new MindeeSettings() - : new MindeeSettings(apiKey); + MindeeSettingsV2 settings = apiKey == null || apiKey.trim().isEmpty() + ? new MindeeSettingsV2() + : new MindeeSettingsV2(apiKey); return MindeeHttpApiV2.builder() .mindeeSettings(settings) .build(); diff --git a/src/main/java/com/mindee/MindeeSettingsV2.java b/src/main/java/com/mindee/MindeeSettingsV2.java index c87547fd9..fb6b0544a 100644 --- a/src/main/java/com/mindee/MindeeSettingsV2.java +++ b/src/main/java/com/mindee/MindeeSettingsV2.java @@ -1,8 +1,7 @@ package com.mindee; -import lombok.Getter; - import java.util.Optional; +import lombok.Getter; /** * Mindee API V2 configuration. diff --git a/src/main/java/com/mindee/PredictOptionsV2.java b/src/main/java/com/mindee/PredictOptionsV2.java deleted file mode 100644 index 69af2e7ce..000000000 --- a/src/main/java/com/mindee/PredictOptionsV2.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.mindee; - -import com.mindee.input.LocalInputSource; -import lombok.Builder; -import lombok.Getter; -import lombok.Singular; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * Parameters for the V2 “predict” endpoint. - * - *

This is a pure config object – no Jackson annotations are used.

- */ -@Getter -public final class PredictOptionsV2 { - - /** - * Optional alias for the file. - */ - private final String alias; - - /** - * ID of the model. - */ - private final String modelId; - - /** - * If {@code true}, enable Retrieval-Augmented Generation. - */ - private final boolean rag; - - /** - * IDs of webhooks to propagate the API response to (may be empty). - */ - private final List webhookIds; - - /** - * Local input source. - */ - private final LocalInputSource localSource; - - public PredictOptionsV2( - LocalInputSource localSource, - String modelId, - boolean rag, - String alias, - List webhookIds - ) { - - this.localSource = Objects.requireNonNull(localSource, "localSource must not be null"); - this.modelId = Objects.requireNonNull(modelId, "modelId must not be null"); - this.rag = rag; - this.alias = alias; - this.webhookIds = webhookIds == null ? Collections.emptyList() - : webhookIds; - } - - @Builder(builderMethodName = "builder") - private static PredictOptionsV2 build( - LocalInputSource localSource, - String modelId, - boolean rag, - String alias, - @Singular List webhookIds - ) { - - return new PredictOptionsV2(localSource, modelId, rag, alias, webhookIds); - } -} diff --git a/src/main/java/com/mindee/http/MindeeApi.java b/src/main/java/com/mindee/http/MindeeApi.java index efd4243a8..bc01d52d7 100644 --- a/src/main/java/com/mindee/http/MindeeApi.java +++ b/src/main/java/com/mindee/http/MindeeApi.java @@ -4,7 +4,6 @@ import com.mindee.parsing.common.Inference; import com.mindee.parsing.common.PredictResponse; import com.mindee.parsing.common.WorkflowResponse; - import java.io.IOException; /** diff --git a/src/main/java/com/mindee/http/MindeeApiCommon.java b/src/main/java/com/mindee/http/MindeeApiCommon.java index dae787fb7..6d1bba5dd 100644 --- a/src/main/java/com/mindee/http/MindeeApiCommon.java +++ b/src/main/java/com/mindee/http/MindeeApiCommon.java @@ -1,12 +1,17 @@ package com.mindee.http; -import org.apache.hc.core5.http.HttpEntity; - import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.apache.hc.core5.http.HttpEntity; +/** + * Defines common methods for mindee APIs. + */ public abstract class MindeeApiCommon { - + /** + * Retrieves the user agent. + * @return the user agent. + */ protected String getUserAgent() { String javaVersion = System.getProperty("java.version"); String sdkVersion = getClass().getPackage().getImplementationVersion(); @@ -28,6 +33,11 @@ protected String getUserAgent() { return String.format("mindee-api-java@v%s java-v%s %s", sdkVersion, javaVersion, osName); } + /** + * Checks if the status code is in the 2xx range. + * @param statusCode the status code to check. + * @return {@code true} if the status code is in the 2xx range, false otherwise. + */ protected boolean is2xxStatusCode(int statusCode) { return statusCode >= 200 && statusCode <= 299; } diff --git a/src/main/java/com/mindee/http/MindeeApiV2.java b/src/main/java/com/mindee/http/MindeeApiV2.java index 8513b7351..201934dff 100644 --- a/src/main/java/com/mindee/http/MindeeApiV2.java +++ b/src/main/java/com/mindee/http/MindeeApiV2.java @@ -1,9 +1,9 @@ package com.mindee.http; -import com.mindee.PredictOptionsV2; -import com.mindee.parsing.v2.AsyncInferenceResponse; +import com.mindee.InferencePredictOptions; +import com.mindee.input.LocalInputSource; import com.mindee.parsing.v2.AsyncJobResponse; - +import com.mindee.parsing.v2.CommonResponse; import java.io.IOException; /** @@ -14,14 +14,14 @@ abstract public class MindeeApiV2 extends MindeeApiCommon { * Send a file to the prediction queue. */ abstract public AsyncJobResponse enqueuePost( - PredictOptionsV2 options, - RequestParameters requestParameters + LocalInputSource inputSource, + InferencePredictOptions options ) throws IOException; /** * Get a document from the predict queue. */ - abstract public AsyncInferenceResponse getInferenceFromQueue( + abstract public CommonResponse getInferenceFromQueue( String jobId ); diff --git a/src/main/java/com/mindee/http/MindeeHttpApiV2.java b/src/main/java/com/mindee/http/MindeeHttpApiV2.java index 4aaaf472d..5186d8d63 100644 --- a/src/main/java/com/mindee/http/MindeeHttpApiV2.java +++ b/src/main/java/com/mindee/http/MindeeHttpApiV2.java @@ -1,15 +1,19 @@ package com.mindee.http; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mindee.InferencePredictOptions; import com.mindee.MindeeException; import com.mindee.MindeeSettingsV2; -import com.mindee.PredictOptionsV2; -import com.mindee.parsing.common.*; +import com.mindee.input.LocalInputSource; import com.mindee.parsing.v2.AsyncInferenceResponse; import com.mindee.parsing.v2.AsyncJobResponse; import com.mindee.parsing.v2.CommonResponse; +import com.mindee.parsing.v2.ErrorResponse; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import lombok.Builder; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; @@ -17,29 +21,22 @@ import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.core5.http.*; -import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.net.URIBuilder; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - /** * HTTP Client class for the V2 API. */ public final class MindeeHttpApiV2 extends MindeeApiV2 { private static final ObjectMapper mapper = new ObjectMapper(); - private final Function buildProductPredicBasetUrl = this::buildProductPredictBaseUrl; - private final Function buildWorkflowPredictBaseUrl = this::buildWorkflowPredictBaseUrl; - private final Function buildWorkflowExecutionBaseUrl = this::buildWorkflowExecutionUrl; + /** * The MindeeSetting needed to make the api call. */ @@ -49,21 +46,11 @@ public final class MindeeHttpApiV2 extends MindeeApiV2 { * Defaults to HttpClientBuilder.create().useSystemProperties() */ private final HttpClientBuilder httpClientBuilder; - /** - * The function used to generate the synchronous API endpoint URL. - * Only needs to be set if the api calls need to be directed through internal URLs. - */ - private final Function enqueuePost; - private final Function urlFromEndpoint; + public MindeeHttpApiV2(MindeeSettingsV2 mindeeSettings) { this( mindeeSettings, - null, - null, - null, - null, - null, null ); } @@ -71,9 +58,7 @@ public MindeeHttpApiV2(MindeeSettingsV2 mindeeSettings) { @Builder private MindeeHttpApiV2( MindeeSettingsV2 mindeeSettings, - HttpClientBuilder httpClientBuilder, - Function enqueuePost, - Function getInferenceFromQueue + HttpClientBuilder httpClientBuilder ) { this.mindeeSettings = mindeeSettings; @@ -82,6 +67,177 @@ private MindeeHttpApiV2( } else { this.httpClientBuilder = HttpClientBuilder.create().useSystemProperties(); } - // TODO + } + + /** + * Enqueues a doc with the POST method. + * + * @param inputSource Input source to send. + * @param options Options to send the file along with. + * @return A job response. + */ + public AsyncJobResponse enqueuePost( + LocalInputSource inputSource, + InferencePredictOptions options + ) { + String url = this.mindeeSettings.getBaseUrl() + "/inferences/enqueue"; + HttpPost post = buildHttpPost(url, inputSource, options); + + mapper.findAndRegisterModules(); + try (CloseableHttpClient httpClient = httpClientBuilder.build()) { + return httpClient.execute( + post, response -> { + String raw = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + return deserializeOrThrow(raw, AsyncJobResponse.class, response.getCode()); + } + ); + } catch (IOException err) { + throw new MindeeException(err.getMessage(), err); + } + } + + public CommonResponse getInferenceFromQueue( + String jobId + ) { + + String url = this.mindeeSettings.getBaseUrl() + "/inferences/" + jobId; + HttpGet get = new HttpGet(url); + + mapper.findAndRegisterModules(); + try (CloseableHttpClient httpClient = httpClientBuilder.build()) { + return httpClient.execute( + get, response -> { + HttpEntity responseEntity = response.getEntity(); + int statusCode = response.getCode(); + if (!is2xxStatusCode(statusCode)) { + throw getHttpError(response); + } + try { + String raw = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + return deserializeOrThrow(raw, AsyncInferenceResponse.class, response.getCode()); + } finally { + /* make sure the connection can be reused even if parsing fails */ + EntityUtils.consumeQuietly(responseEntity); + } + + } + ); + } catch (IOException err) { + throw new MindeeException(err.getMessage(), err); + } + } + + private List buildPostParams( + InferencePredictOptions options + ) { + ArrayList params = new ArrayList(); + params.add(new BasicNameValuePair("model_id", options.getModelId())); + if (options.isFullText()) { + params.add(new BasicNameValuePair("full_text_ocr", "true")); + } + if (options.isRag()) { + params.add(new BasicNameValuePair("rag", "true")); + } + if (options.getAlias() != null) { + params.add(new BasicNameValuePair("alias", options.getAlias())); + } + if (!options.getWebhookIds().isEmpty()) { + params.add(new BasicNameValuePair("webhook_ids", String.join(",", options.getWebhookIds()))); + } + return params; + } + + private MindeeHttpExceptionV2 getHttpError(ClassicHttpResponse response) { + String rawBody; + try { + rawBody = response.getEntity() == null + ? "" + : EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + ErrorResponse err = mapper.readValue(rawBody, ErrorResponse.class); + + if (err.getDetail() == null) { + err = new ErrorResponse("Unknown error", response.getCode()); + } + return new MindeeHttpExceptionV2(err.getStatus(), err.getDetail()); + + } catch (Exception e) { + return new MindeeHttpExceptionV2(response.getCode(), "Unknown error"); + } + } + + + private HttpEntity buildHttpBody( + LocalInputSource inputSource, + InferencePredictOptions options + ) { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.EXTENDED); + builder.addBinaryBody( + "document", + inputSource.getFile(), + ContentType.DEFAULT_BINARY, + inputSource.getFilename() + ); + + if (options.getAlias() != null) { + builder.addTextBody( + "alias", + options.getAlias().toLowerCase() + ); + } + return builder.build(); + } + + + private HttpPost buildHttpPost( + String url, + LocalInputSource inputSource, + InferencePredictOptions options + ) { + HttpPost post; + try { + URIBuilder uriBuilder = new URIBuilder(url); + uriBuilder.addParameters(buildPostParams(options)); + post = new HttpPost(uriBuilder.build()); + } + // This exception will never happen because we are providing the URL internally. + // Do this to avoid declaring the exception in the method signature. + catch (URISyntaxException err) { + return new HttpPost("invalid URI"); + } + + if (this.mindeeSettings.getApiKey().isPresent()) { + post.setHeader(HttpHeaders.AUTHORIZATION, this.mindeeSettings.getApiKey().get()); + } + post.setHeader(HttpHeaders.USER_AGENT, getUserAgent()); + post.setEntity(buildHttpBody(inputSource, options)); + return post; + } + + + private R deserializeOrThrow( + String body, Class clazz, int httpStatus) throws MindeeHttpExceptionV2 { + + if (httpStatus >= 200 && httpStatus < 300) { + try { + R model = mapper.readValue(body, clazz); + model.setRawResponse(body); + return model; + } catch (Exception ignored) { + } + } + + ErrorResponse err; + try { + err = mapper.readValue(body, ErrorResponse.class); + if (err.getDetail() == null) { + err = new ErrorResponse("Unknown error", httpStatus); + } + } catch (Exception ignored) { + err = new ErrorResponse("Unknown error", httpStatus); + } + throw new MindeeHttpExceptionV2(err.getStatus(), err.getDetail()); } } diff --git a/src/main/java/com/mindee/http/MindeeHttpExceptionV2.java b/src/main/java/com/mindee/http/MindeeHttpExceptionV2.java new file mode 100644 index 000000000..056a72747 --- /dev/null +++ b/src/main/java/com/mindee/http/MindeeHttpExceptionV2.java @@ -0,0 +1,29 @@ +package com.mindee.http; + +import com.mindee.MindeeException; +import lombok.Getter; + +/** + * Represent a Mindee exception. + */ +@Getter +public class MindeeHttpExceptionV2 extends MindeeException { + /** Standard HTTP status code. */ + private final int status; + /** Error details. */ + private final String detail; + + public MindeeHttpExceptionV2(int status, String detail) { + super(detail); + this.status = status; + this.detail = detail; + } + + public String toString() { + String outStr = super.toString() + " - HTTP " + getStatus(); + if (!getDetail().isEmpty()) { + outStr += " - " + getDetail(); + } + return outStr; + } +} diff --git a/src/main/java/com/mindee/input/PageOptions.java b/src/main/java/com/mindee/input/PageOptions.java index de344640c..3de632cec 100644 --- a/src/main/java/com/mindee/input/PageOptions.java +++ b/src/main/java/com/mindee/input/PageOptions.java @@ -46,4 +46,31 @@ public PageOptions( this.operation = operation; this.onMinPages = onMinPages; } + + /** + * Builder for page options. + */ + public static final class Builder { + private List pageIndexes; + private PageOptionsOperation operation; + private Integer onMinPages; + + public Builder pageIndexes(List pageIndexes) { + this.pageIndexes = pageIndexes; + return this; + } + + public Builder operation(PageOptionsOperation operation) { + this.operation = operation; + return this; + } + public Builder onMinPages(Integer onMinPages) { + this.onMinPages = onMinPages; + return this; + } + + public PageOptions build() { + return new PageOptions(pageIndexes, operation, onMinPages); + } + } } diff --git a/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java b/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java index 18f35e64f..affbf02af 100644 --- a/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java +++ b/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java @@ -2,6 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; +/** + * Represents an asynchronous inference response (V2). + */ public class AsyncInferenceResponse extends CommonResponse { /** diff --git a/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java b/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java index 31090785a..dbec81b22 100644 --- a/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java +++ b/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; - import java.io.IOException; /** diff --git a/src/main/java/com/mindee/parsing/v2/Inference.java b/src/main/java/com/mindee/parsing/v2/Inference.java index a23bcbf32..e132afc14 100644 --- a/src/main/java/com/mindee/parsing/v2/Inference.java +++ b/src/main/java/com/mindee/parsing/v2/Inference.java @@ -2,13 +2,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.StringJoiner; - /** * Inference object for the V2 API. */ diff --git a/src/main/java/com/mindee/parsing/v2/InferenceFields.java b/src/main/java/com/mindee/parsing/v2/InferenceFields.java index 1670494b1..e249d5a90 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceFields.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceFields.java @@ -1,9 +1,8 @@ package com.mindee.parsing.v2; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.EqualsAndHashCode; - import java.util.HashMap; +import lombok.EqualsAndHashCode; /** * Inference fields map. diff --git a/src/main/java/com/mindee/parsing/v2/InferenceOptions.java b/src/main/java/com/mindee/parsing/v2/InferenceOptions.java index 1c675b0c4..92fb5351e 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceOptions.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceOptions.java @@ -1,4 +1,15 @@ package com.mindee.parsing.v2; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; + +/** + * Option response for V2 API inference. + */ +@Getter public final class InferenceOptions { + + @JsonProperty("raw_text") + private List rawText; } diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResult.java b/src/main/java/com/mindee/parsing/v2/InferenceResult.java index ef0521283..b56c51064 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceResult.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResult.java @@ -2,14 +2,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.Map; -import java.util.StringJoiner; - /** * Generic result for any off-the-shelf Mindee V2 model. */ diff --git a/src/main/java/com/mindee/parsing/v2/Job.java b/src/main/java/com/mindee/parsing/v2/Job.java index f35841ea8..b48dadb05 100644 --- a/src/main/java/com/mindee/parsing/v2/Job.java +++ b/src/main/java/com/mindee/parsing/v2/Job.java @@ -4,14 +4,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.mindee.parsing.common.LocalDateTimeDeserializer; +import java.time.LocalDateTime; +import java.util.List; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; -import java.util.List; - /** * Defines an enqueued Job. */ diff --git a/src/main/java/com/mindee/parsing/v2/ListField.java b/src/main/java/com/mindee/parsing/v2/ListField.java index 3c7c080e3..d19ff0af1 100644 --- a/src/main/java/com/mindee/parsing/v2/ListField.java +++ b/src/main/java/com/mindee/parsing/v2/ListField.java @@ -2,15 +2,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.List; -import java.util.StringJoiner; - /** * Field holding a list of fields. */ diff --git a/src/main/java/com/mindee/parsing/v2/ObjectField.java b/src/main/java/com/mindee/parsing/v2/ObjectField.java index 3e057cbdd..819a1e178 100644 --- a/src/main/java/com/mindee/parsing/v2/ObjectField.java +++ b/src/main/java/com/mindee/parsing/v2/ObjectField.java @@ -2,14 +2,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.Map; -import java.util.StringJoiner; - /** * Field holding a map of sub-fields. */ diff --git a/src/main/java/com/mindee/parsing/v2/Webhook.java b/src/main/java/com/mindee/parsing/v2/Webhook.java index 6fd5aa613..c58af38ec 100644 --- a/src/main/java/com/mindee/parsing/v2/Webhook.java +++ b/src/main/java/com/mindee/parsing/v2/Webhook.java @@ -4,13 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.mindee.parsing.common.LocalDateTimeDeserializer; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - /** * Webhook info. */ From b31cb2e46f15ccbea678d62617f2fae014bd6649 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:58:14 +0200 Subject: [PATCH 03/12] add sample code --- docs/code_samples/default_v2.txt | 44 +++++++++++++++++++ src/main/java/com/mindee/MindeeClientV2.java | 22 +++++----- .../java/com/mindee/http/MindeeApiV2.java | 4 +- .../java/com/mindee/http/MindeeHttpApiV2.java | 10 ++--- ...ceResponse.java => InferenceResponse.java} | 2 +- ...AsyncJobResponse.java => JobResponse.java} | 2 +- tests/test_code_samples.sh | 17 +++++++ 7 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 docs/code_samples/default_v2.txt rename src/main/java/com/mindee/parsing/v2/{AsyncInferenceResponse.java => InferenceResponse.java} (80%) rename src/main/java/com/mindee/parsing/v2/{AsyncJobResponse.java => JobResponse.java} (87%) diff --git a/docs/code_samples/default_v2.txt b/docs/code_samples/default_v2.txt new file mode 100644 index 000000000..17488eb1b --- /dev/null +++ b/docs/code_samples/default_v2.txt @@ -0,0 +1,44 @@ +import com.mindee.MindeeClientV2; +import com.mindee.InferencePredictOptions; +import com.mindee.input.LocalInputSource; +import com.mindee.parsing.common.InferenceResponse; +import java.io.File; +import java.io.IOException; + +public class SimpleMindeeClient { + + public static void main(String[] args) throws IOException { + String apiKey = "MY_API_KEY"; + String filePath = "/path/to/the/file.ext"; + + // Init a new client + MindeeClientV2 mindeeClient = new MindeeClientV2(apiKey); + + // Load a file from disk + LocalInputSource inputSource = new LocalInputSource(new File(filePath)); + + // Prepare the enqueueing options + InferencePredictOptions options = InferencePredictOptions.builder().modelId("MY_MODEL_ID").build(); + + // Parse the file + InferenceResponse response = mindeeClient.enqueueAndParse( + inputSource; + options + ); + + // Print a summary of the response + System.out.println(response.toString()); + + // Print a summary of the predictions +// System.out.println(response.getDocument().toString()); + + // Print the document-level predictions +// System.out.println(response.getDocument().getInference().getPrediction().toString()); + + // Print the page-level predictions +// response.getDocument().getInference().getPages().forEach( +// page -> System.out.println(page.toString()) +// ); + } + +} diff --git a/src/main/java/com/mindee/MindeeClientV2.java b/src/main/java/com/mindee/MindeeClientV2.java index f4624e610..d41d76c20 100644 --- a/src/main/java/com/mindee/MindeeClientV2.java +++ b/src/main/java/com/mindee/MindeeClientV2.java @@ -5,8 +5,8 @@ import com.mindee.http.MindeeHttpApiV2; import com.mindee.input.LocalInputSource; import com.mindee.input.LocalResponse; -import com.mindee.parsing.v2.AsyncInferenceResponse; -import com.mindee.parsing.v2.AsyncJobResponse; +import com.mindee.parsing.v2.InferenceResponse; +import com.mindee.parsing.v2.JobResponse; import com.mindee.parsing.v2.CommonResponse; import com.mindee.pdf.PdfBoxApi; import com.mindee.pdf.PdfOperation; @@ -42,7 +42,7 @@ public MindeeClientV2(PdfOperation pdfOperation, MindeeApiV2 mindeeApi) { /** * Enqueue a document in the asynchronous “Generated” queue. */ - public AsyncJobResponse enqueue( + public JobResponse enqueue( LocalInputSource inputSource, InferencePredictOptions options) throws IOException { LocalInputSource finalInput; @@ -67,7 +67,7 @@ public CommonResponse parseQueued(String jobId) { /** * Convenience helper: enqueue, poll, and return the final inference. */ - public AsyncInferenceResponse enqueueAndParse( + public InferenceResponse enqueueAndParse( LocalInputSource inputSource, InferencePredictOptions options, AsyncPollingOptions polling) throws IOException, InterruptedException { @@ -77,7 +77,7 @@ public AsyncInferenceResponse enqueueAndParse( } validatePollingOptions(polling); - AsyncJobResponse job = enqueue(inputSource, options); + JobResponse job = enqueue(inputSource, options); Thread.sleep((long) (polling.getInitialDelaySec() * 1000)); @@ -86,8 +86,8 @@ public AsyncInferenceResponse enqueueAndParse( while (attempts < max) { Thread.sleep((long) (polling.getIntervalSec() * 1000)); CommonResponse resp = parseQueued(job.getJob().getId()); - if (resp instanceof AsyncInferenceResponse) { - return (AsyncInferenceResponse) resp; + if (resp instanceof InferenceResponse) { + return (InferenceResponse) resp; } attempts++; } @@ -96,12 +96,12 @@ public AsyncInferenceResponse enqueueAndParse( /** * Deserialize a webhook payload (or any saved response) into - * {@link AsyncInferenceResponse}. + * {@link InferenceResponse}. */ - public AsyncInferenceResponse loadInference(LocalResponse localResponse) throws IOException { + public InferenceResponse loadInference(LocalResponse localResponse) throws IOException { ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); - AsyncInferenceResponse model = - mapper.readValue(localResponse.getFile(), AsyncInferenceResponse.class); + InferenceResponse model = + mapper.readValue(localResponse.getFile(), InferenceResponse.class); model.setRawResponse(localResponse.toString()); return model; } diff --git a/src/main/java/com/mindee/http/MindeeApiV2.java b/src/main/java/com/mindee/http/MindeeApiV2.java index 201934dff..cc381478f 100644 --- a/src/main/java/com/mindee/http/MindeeApiV2.java +++ b/src/main/java/com/mindee/http/MindeeApiV2.java @@ -2,7 +2,7 @@ import com.mindee.InferencePredictOptions; import com.mindee.input.LocalInputSource; -import com.mindee.parsing.v2.AsyncJobResponse; +import com.mindee.parsing.v2.JobResponse; import com.mindee.parsing.v2.CommonResponse; import java.io.IOException; @@ -13,7 +13,7 @@ abstract public class MindeeApiV2 extends MindeeApiCommon { /** * Send a file to the prediction queue. */ - abstract public AsyncJobResponse enqueuePost( + abstract public JobResponse enqueuePost( LocalInputSource inputSource, InferencePredictOptions options ) throws IOException; diff --git a/src/main/java/com/mindee/http/MindeeHttpApiV2.java b/src/main/java/com/mindee/http/MindeeHttpApiV2.java index 5186d8d63..843998923 100644 --- a/src/main/java/com/mindee/http/MindeeHttpApiV2.java +++ b/src/main/java/com/mindee/http/MindeeHttpApiV2.java @@ -5,8 +5,8 @@ import com.mindee.MindeeException; import com.mindee.MindeeSettingsV2; import com.mindee.input.LocalInputSource; -import com.mindee.parsing.v2.AsyncInferenceResponse; -import com.mindee.parsing.v2.AsyncJobResponse; +import com.mindee.parsing.v2.InferenceResponse; +import com.mindee.parsing.v2.JobResponse; import com.mindee.parsing.v2.CommonResponse; import com.mindee.parsing.v2.ErrorResponse; import java.io.IOException; @@ -76,7 +76,7 @@ private MindeeHttpApiV2( * @param options Options to send the file along with. * @return A job response. */ - public AsyncJobResponse enqueuePost( + public JobResponse enqueuePost( LocalInputSource inputSource, InferencePredictOptions options ) { @@ -88,7 +88,7 @@ public AsyncJobResponse enqueuePost( return httpClient.execute( post, response -> { String raw = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); - return deserializeOrThrow(raw, AsyncJobResponse.class, response.getCode()); + return deserializeOrThrow(raw, JobResponse.class, response.getCode()); } ); } catch (IOException err) { @@ -115,7 +115,7 @@ public CommonResponse getInferenceFromQueue( try { String raw = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); - return deserializeOrThrow(raw, AsyncInferenceResponse.class, response.getCode()); + return deserializeOrThrow(raw, InferenceResponse.class, response.getCode()); } finally { /* make sure the connection can be reused even if parsing fails */ EntityUtils.consumeQuietly(responseEntity); diff --git a/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java b/src/main/java/com/mindee/parsing/v2/InferenceResponse.java similarity index 80% rename from src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java rename to src/main/java/com/mindee/parsing/v2/InferenceResponse.java index affbf02af..c78f5800b 100644 --- a/src/main/java/com/mindee/parsing/v2/AsyncInferenceResponse.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResponse.java @@ -5,7 +5,7 @@ /** * Represents an asynchronous inference response (V2). */ -public class AsyncInferenceResponse extends CommonResponse { +public class InferenceResponse extends CommonResponse { /** * Inference result. diff --git a/src/main/java/com/mindee/parsing/v2/AsyncJobResponse.java b/src/main/java/com/mindee/parsing/v2/JobResponse.java similarity index 87% rename from src/main/java/com/mindee/parsing/v2/AsyncJobResponse.java rename to src/main/java/com/mindee/parsing/v2/JobResponse.java index f2d1407cc..dcad0c80a 100644 --- a/src/main/java/com/mindee/parsing/v2/AsyncJobResponse.java +++ b/src/main/java/com/mindee/parsing/v2/JobResponse.java @@ -12,7 +12,7 @@ @Data @EqualsAndHashCode(callSuper = true) @JsonIgnoreProperties(ignoreUnknown = true) -public final class AsyncJobResponse extends CommonResponse { +public final class JobResponse extends CommonResponse { /** * Representation of the Job. */ diff --git a/tests/test_code_samples.sh b/tests/test_code_samples.sh index 43e00b2bf..807b02adb 100755 --- a/tests/test_code_samples.sh +++ b/tests/test_code_samples.sh @@ -5,6 +5,8 @@ OUTPUT_FILE='SimpleMindeeClient.java' ACCOUNT=$1 ENDPOINT=$2 API_KEY=$3 +API_KEY_V2=$4 +MODEL_ID=$5 if [ -z "${ACCOUNT}" ]; then echo "ACCOUNT is required"; exit 1; fi if [ -z "${ENDPOINT}" ]; then echo "ENDPOINT is required"; exit 1; fi @@ -14,6 +16,13 @@ mvn dependency:copy-dependencies for f in $(find docs/code_samples -maxdepth 1 -name "*.txt" -not -name "workflow_execution.txt" | sort -h) do + if echo "${f}" | grep -q "default_v2.txt"; then + if [ -z "${API_KEY_V2}" ] || [ -z "${MODEL_ID}" ]; then + echo "Skipping ${f} (API_KEY_V2 or MODEL_ID not supplied)" + echo + continue + fi + fi echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "${f}" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" @@ -41,6 +50,14 @@ do sed -i "s/my-version/1/" $OUTPUT_FILE fi + if echo "${f}" | grep -q "default_v2.txt" + then + sed -i "s/MY_API_KEY/$API_KEY_V2/" $OUTPUT_FILE + sed -i "s/MY_MODEL_ID/$MODEL_ID/" $OUTPUT_FILE + else + sed -i "s/my-api-key/$API_KEY/" $OUTPUT_FILE + fi + sed -i "s/my-api-key/$API_KEY/" $OUTPUT_FILE sed -i "s/\/path\/to\/the\/file.ext/src\/test\/resources\/file_types\/pdf\/blank_1.pdf/" $OUTPUT_FILE From d34365e0dcd8e4cf0a26d050c18e46df56ac0c6c Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:56:24 +0200 Subject: [PATCH 04/12] fix lingering issues (NO TESTS) --- docs/code_samples/default_v2.txt | 11 ++-- .../com/mindee/InferencePredictOptions.java | 2 +- src/main/java/com/mindee/MindeeClientV2.java | 23 ++++---- .../java/com/mindee/MindeeSettingsV2.java | 4 +- .../java/com/mindee/http/MindeeApiV2.java | 2 +- .../java/com/mindee/http/MindeeHttpApiV2.java | 55 ++++++++----------- .../mindee/parsing/v2/InferenceFields.java | 9 +++ .../mindee/parsing/v2/InferenceResponse.java | 2 + .../mindee/parsing/v2/InferenceResult.java | 17 +++--- src/main/java/com/mindee/parsing/v2/Job.java | 2 +- .../java/com/mindee/parsing/v2/ListField.java | 9 ++- .../com/mindee/parsing/v2/ObjectField.java | 32 ++++++++--- .../com/mindee/parsing/v2/SimpleField.java | 2 +- 13 files changed, 98 insertions(+), 72 deletions(-) diff --git a/docs/code_samples/default_v2.txt b/docs/code_samples/default_v2.txt index 17488eb1b..3aff5904c 100644 --- a/docs/code_samples/default_v2.txt +++ b/docs/code_samples/default_v2.txt @@ -1,13 +1,13 @@ import com.mindee.MindeeClientV2; import com.mindee.InferencePredictOptions; import com.mindee.input.LocalInputSource; -import com.mindee.parsing.common.InferenceResponse; +import com.mindee.parsing.v2.InferenceResponse; import java.io.File; import java.io.IOException; public class SimpleMindeeClient { - public static void main(String[] args) throws IOException { + public static void main(String[] args) throws IOException, InterruptedException { String apiKey = "MY_API_KEY"; String filePath = "/path/to/the/file.ext"; @@ -18,16 +18,17 @@ public class SimpleMindeeClient { LocalInputSource inputSource = new LocalInputSource(new File(filePath)); // Prepare the enqueueing options - InferencePredictOptions options = InferencePredictOptions.builder().modelId("MY_MODEL_ID").build(); + // Note: modelId is mandatory. + InferencePredictOptions options = InferencePredictOptions.builder("MY_MODEL_ID").build(); // Parse the file InferenceResponse response = mindeeClient.enqueueAndParse( - inputSource; + inputSource, options ); // Print a summary of the response - System.out.println(response.toString()); + System.out.println(response.getInference().toString()); // Print a summary of the predictions // System.out.println(response.getDocument().toString()); diff --git a/src/main/java/com/mindee/InferencePredictOptions.java b/src/main/java/com/mindee/InferencePredictOptions.java index 5a0371498..667ec1aa7 100644 --- a/src/main/java/com/mindee/InferencePredictOptions.java +++ b/src/main/java/com/mindee/InferencePredictOptions.java @@ -65,7 +65,7 @@ public static final class Builder { private String alias; private List webhookIds = Collections.emptyList(); private PageOptions pageOptions = null; - private AsyncPollingOptions pollingOptions = null; + private AsyncPollingOptions pollingOptions = AsyncPollingOptions.builder().build(); private Builder(String modelId) { this.modelId = Objects.requireNonNull(modelId, "modelId must not be null"); diff --git a/src/main/java/com/mindee/MindeeClientV2.java b/src/main/java/com/mindee/MindeeClientV2.java index d41d76c20..94ef7dbdf 100644 --- a/src/main/java/com/mindee/MindeeClientV2.java +++ b/src/main/java/com/mindee/MindeeClientV2.java @@ -5,9 +5,9 @@ import com.mindee.http.MindeeHttpApiV2; import com.mindee.input.LocalInputSource; import com.mindee.input.LocalResponse; +import com.mindee.parsing.v2.CommonResponse; import com.mindee.parsing.v2.InferenceResponse; import com.mindee.parsing.v2.JobResponse; -import com.mindee.parsing.v2.CommonResponse; import com.mindee.pdf.PdfBoxApi; import com.mindee.pdf.PdfOperation; import java.io.IOException; @@ -65,26 +65,27 @@ public CommonResponse parseQueued(String jobId) { } /** - * Convenience helper: enqueue, poll, and return the final inference. + * Send a local file to an async queue, poll, and parse when complete. + * @param inputSource The input source to send. + * @param options The options to send along with the file. + * @return an instance of {@link InferenceResponse}. + * @throws IOException Throws if the file can't be accessed. + * @throws InterruptedException Throws if the thread is interrupted. */ public InferenceResponse enqueueAndParse( LocalInputSource inputSource, - InferencePredictOptions options, - AsyncPollingOptions polling) throws IOException, InterruptedException { + InferencePredictOptions options) throws IOException, InterruptedException { - if (polling == null) { - polling = AsyncPollingOptions.builder().build(); // default values - } - validatePollingOptions(polling); + validatePollingOptions(options.getPollingOptions()); JobResponse job = enqueue(inputSource, options); - Thread.sleep((long) (polling.getInitialDelaySec() * 1000)); + Thread.sleep((long) (options.getPollingOptions().getInitialDelaySec() * 1000)); int attempts = 0; - int max = polling.getMaxRetries(); + int max = options.getPollingOptions().getMaxRetries(); while (attempts < max) { - Thread.sleep((long) (polling.getIntervalSec() * 1000)); + Thread.sleep((long) (options.getPollingOptions().getIntervalSec() * 1000)); CommonResponse resp = parseQueued(job.getJob().getId()); if (resp instanceof InferenceResponse) { return (InferenceResponse) resp; diff --git a/src/main/java/com/mindee/MindeeSettingsV2.java b/src/main/java/com/mindee/MindeeSettingsV2.java index fb6b0544a..3d95ba56a 100644 --- a/src/main/java/com/mindee/MindeeSettingsV2.java +++ b/src/main/java/com/mindee/MindeeSettingsV2.java @@ -1,15 +1,17 @@ package com.mindee; import java.util.Optional; +import lombok.Builder; import lombok.Getter; /** * Mindee API V2 configuration. */ @Getter +@Builder public class MindeeSettingsV2 { - private static final String DEFAULT_MINDEE_V2_API_URL = "https://api-v2.mindee.net/v1"; + private static final String DEFAULT_MINDEE_V2_API_URL = "https://api-v2.mindee.net/v2"; private final String apiKey; private final String baseUrl; diff --git a/src/main/java/com/mindee/http/MindeeApiV2.java b/src/main/java/com/mindee/http/MindeeApiV2.java index cc381478f..5ce244bf9 100644 --- a/src/main/java/com/mindee/http/MindeeApiV2.java +++ b/src/main/java/com/mindee/http/MindeeApiV2.java @@ -2,8 +2,8 @@ import com.mindee.InferencePredictOptions; import com.mindee.input.LocalInputSource; -import com.mindee.parsing.v2.JobResponse; import com.mindee.parsing.v2.CommonResponse; +import com.mindee.parsing.v2.JobResponse; import java.io.IOException; /** diff --git a/src/main/java/com/mindee/http/MindeeHttpApiV2.java b/src/main/java/com/mindee/http/MindeeHttpApiV2.java index 843998923..1cae5ba82 100644 --- a/src/main/java/com/mindee/http/MindeeHttpApiV2.java +++ b/src/main/java/com/mindee/http/MindeeHttpApiV2.java @@ -5,15 +5,13 @@ import com.mindee.MindeeException; import com.mindee.MindeeSettingsV2; import com.mindee.input.LocalInputSource; -import com.mindee.parsing.v2.InferenceResponse; -import com.mindee.parsing.v2.JobResponse; import com.mindee.parsing.v2.CommonResponse; import com.mindee.parsing.v2.ErrorResponse; +import com.mindee.parsing.v2.InferenceResponse; +import com.mindee.parsing.v2.JobResponse; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; import lombok.Builder; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; @@ -25,9 +23,7 @@ import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.net.URIBuilder; /** @@ -103,6 +99,10 @@ public CommonResponse getInferenceFromQueue( String url = this.mindeeSettings.getBaseUrl() + "/inferences/" + jobId; HttpGet get = new HttpGet(url); + if (this.mindeeSettings.getApiKey().isPresent()) { + get.setHeader(HttpHeaders.AUTHORIZATION, this.mindeeSettings.getApiKey().get()); + } + get.setHeader(HttpHeaders.USER_AGENT, getUserAgent()); mapper.findAndRegisterModules(); try (CloseableHttpClient httpClient = httpClientBuilder.build()) { return httpClient.execute( @@ -120,7 +120,6 @@ public CommonResponse getInferenceFromQueue( /* make sure the connection can be reused even if parsing fails */ EntityUtils.consumeQuietly(responseEntity); } - } ); } catch (IOException err) { @@ -128,26 +127,6 @@ public CommonResponse getInferenceFromQueue( } } - private List buildPostParams( - InferencePredictOptions options - ) { - ArrayList params = new ArrayList(); - params.add(new BasicNameValuePair("model_id", options.getModelId())); - if (options.isFullText()) { - params.add(new BasicNameValuePair("full_text_ocr", "true")); - } - if (options.isRag()) { - params.add(new BasicNameValuePair("rag", "true")); - } - if (options.getAlias() != null) { - params.add(new BasicNameValuePair("alias", options.getAlias())); - } - if (!options.getWebhookIds().isEmpty()) { - params.add(new BasicNameValuePair("webhook_ids", String.join(",", options.getWebhookIds()))); - } - return params; - } - private MindeeHttpExceptionV2 getHttpError(ClassicHttpResponse response) { String rawBody; try { @@ -175,7 +154,7 @@ private HttpEntity buildHttpBody( MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.setMode(HttpMultipartMode.EXTENDED); builder.addBinaryBody( - "document", + "file", inputSource.getFile(), ContentType.DEFAULT_BINARY, inputSource.getFilename() @@ -187,6 +166,20 @@ private HttpEntity buildHttpBody( options.getAlias().toLowerCase() ); } + + builder.addTextBody("model_id", options.getModelId()); + if (options.isFullText()) { + builder.addTextBody("full_text_ocr", "true"); + } + if (options.isRag()) { + builder.addTextBody("rag", "true"); + } + if (options.getAlias() != null) { + builder.addTextBody("alias", options.getAlias()); + } + if (!options.getWebhookIds().isEmpty()) { + builder.addTextBody("webhook_ids", String.join(",", options.getWebhookIds())); + } return builder.build(); } @@ -199,7 +192,6 @@ private HttpPost buildHttpPost( HttpPost post; try { URIBuilder uriBuilder = new URIBuilder(url); - uriBuilder.addParameters(buildPostParams(options)); post = new HttpPost(uriBuilder.build()); } // This exception will never happen because we are providing the URL internally. @@ -222,10 +214,11 @@ private R deserializeOrThrow( if (httpStatus >= 200 && httpStatus < 300) { try { - R model = mapper.readValue(body, clazz); + R model = mapper.readerFor(clazz).readValue(body); model.setRawResponse(body); return model; - } catch (Exception ignored) { + } catch (Exception exception) { + throw new MindeeException("Couldn't deserialize server response:\n" + exception.getMessage()); } } diff --git a/src/main/java/com/mindee/parsing/v2/InferenceFields.java b/src/main/java/com/mindee/parsing/v2/InferenceFields.java index e249d5a90..246da67f1 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceFields.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceFields.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.util.HashMap; +import java.util.Map; import lombok.EqualsAndHashCode; /** @@ -10,4 +11,12 @@ @EqualsAndHashCode(callSuper = true) @JsonIgnoreProperties(ignoreUnknown = true) public final class InferenceFields extends HashMap { + @Override + public String toString() { + StringBuilder strBuilder = new StringBuilder(); + for (Map.Entry entry : this.entrySet()) { + strBuilder.append(':').append(entry.getKey()).append(": ").append(entry.getValue()); + } + return strBuilder.toString(); + } } diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResponse.java b/src/main/java/com/mindee/parsing/v2/InferenceResponse.java index c78f5800b..7c5294c43 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceResponse.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResponse.java @@ -1,10 +1,12 @@ package com.mindee.parsing.v2; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; /** * Represents an asynchronous inference response (V2). */ +@Getter public class InferenceResponse extends CommonResponse { /** diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResult.java b/src/main/java/com/mindee/parsing/v2/InferenceResult.java index b56c51064..81013c889 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceResult.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResult.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Map; import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -19,22 +18,24 @@ @NoArgsConstructor public final class InferenceResult { - /** Model fields. */ + /** + * Model fields. + */ @JsonProperty("fields") private InferenceFields fields; - /** Options. */ + /** + * Options. + */ @JsonProperty("options") private InferenceOptions options; @Override public String toString() { - if (fields == null || fields.isEmpty()) { - return ""; - } StringJoiner joiner = new StringJoiner("\n"); - for (Map.Entry e : fields.entrySet()) { - joiner.add(":" + e.getKey() + ": " + e.getValue()); + joiner.add(fields.toString()); + if (options != null) { + joiner.add(options.toString()); } return joiner.toString(); } diff --git a/src/main/java/com/mindee/parsing/v2/Job.java b/src/main/java/com/mindee/parsing/v2/Job.java index b48dadb05..a96d01f60 100644 --- a/src/main/java/com/mindee/parsing/v2/Job.java +++ b/src/main/java/com/mindee/parsing/v2/Job.java @@ -61,7 +61,7 @@ public final class Job { /** * Optional alias of the file. */ - @JsonProperty("file_name") + @JsonProperty("file_alias") private String fileAlias; /** diff --git a/src/main/java/com/mindee/parsing/v2/ListField.java b/src/main/java/com/mindee/parsing/v2/ListField.java index d19ff0af1..0d981154b 100644 --- a/src/main/java/com/mindee/parsing/v2/ListField.java +++ b/src/main/java/com/mindee/parsing/v2/ListField.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; import java.util.List; -import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -29,10 +28,10 @@ public final class ListField extends BaseField { @Override public String toString() { if (items == null || items.isEmpty()) { - return ""; + return "\n"; } - StringJoiner joiner = new StringJoiner("\n"); - items.forEach(f -> joiner.add(f == null ? "null" : f.toString())); - return joiner.toString(); + StringBuilder strBuilder = new StringBuilder(); + items.forEach(f -> strBuilder.append(f == null ? "" : f.toString())); + return strBuilder.toString(); } } diff --git a/src/main/java/com/mindee/parsing/v2/ObjectField.java b/src/main/java/com/mindee/parsing/v2/ObjectField.java index 819a1e178..b0d641546 100644 --- a/src/main/java/com/mindee/parsing/v2/ObjectField.java +++ b/src/main/java/com/mindee/parsing/v2/ObjectField.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Map; -import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -23,13 +21,33 @@ public class ObjectField extends BaseField { * Sub-fields keyed by their name. */ @JsonProperty("fields") - private Map fields; + private InferenceFields fields; @Override public String toString() { - if (fields == null || fields.isEmpty()) return ""; - StringJoiner joiner = new StringJoiner("\n"); - fields.forEach((k, v) -> joiner.add(k + ": " + v)); - return joiner.toString(); + if (fields == null || fields.isEmpty()) { + return "\n"; + } + + StringBuilder outStr = new StringBuilder(); + + fields.forEach((fieldKey, fieldValue) -> { + outStr.append("\n"); + outStr.append(':').append(fieldKey).append(": "); + + if (fieldValue.getListField() != null) { + ListField listField = fieldValue.getListField(); + if (listField.getItems() != null && !listField.getItems().isEmpty()) { + outStr.append(listField); + } + } else if (fieldValue.getObjectField() != null) { + outStr.append(fieldValue.getObjectField()); + } else if (fieldValue.getSimpleField() != null) { + outStr.append(fieldValue.getSimpleField().getValue() != null ? fieldValue.getSimpleField().getValue() : ""); + } + }); + + return outStr + "\n"; } + } diff --git a/src/main/java/com/mindee/parsing/v2/SimpleField.java b/src/main/java/com/mindee/parsing/v2/SimpleField.java index 03f2ed1a9..88b5f1cda 100644 --- a/src/main/java/com/mindee/parsing/v2/SimpleField.java +++ b/src/main/java/com/mindee/parsing/v2/SimpleField.java @@ -27,6 +27,6 @@ public final class SimpleField extends BaseField { @Override public String toString() { - return value == null ? "null" : value.toString(); + return value == null ? "\n" : (value + "\n"); } } From 52742d5e16cb22aa80fadd39032a6c1b802764f8 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:02:03 +0200 Subject: [PATCH 05/12] fix PR trigger target --- .github/workflows/pull-request.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f2ee318e7..f91b8bf91 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -3,21 +3,25 @@ name: Pull Request on: pull_request: +permissions: + contents: read + pull-requests: read + jobs: static_analysis: - uses: mindee/mindee-api-java/.github/workflows/_static-analysis.yml@main + uses: ./.github/workflows/_static-analysis.yml build: - uses: mindee/mindee-api-java/.github/workflows/_build.yml@main + uses: ./.github/workflows/_build.yml needs: static_analysis secrets: inherit codeql: - uses: mindee/mindee-api-java/.github/workflows/_codeql.yml@main + uses: ./.github/workflows/_codeql.yml needs: build test_integrations: - uses: mindee/mindee-api-java/.github/workflows/_test-integrations.yml@main + uses: ./.github/workflows/_test-integrations.yml needs: build secrets: inherit test_code_samples: - uses: mindee/mindee-api-java/.github/workflows/_test-code-samples.yml@main + uses: ./.github/workflows/_test-code-samples.yml needs: build secrets: inherit From e15675bc416fee72040e0cb47944769e62a68448 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:43:36 +0200 Subject: [PATCH 06/12] fix display + add basic unit test --- .../mindee/parsing/v2/InferenceFields.java | 29 +++- .../com/mindee/parsing/v2/ObjectField.java | 24 +-- .../com/mindee/parsing/v2/SimpleField.java | 2 +- .../java/com/mindee/v2/InferenceTest.java | 138 ++++++++++++++++++ 4 files changed, 164 insertions(+), 29 deletions(-) create mode 100644 src/test/java/com/mindee/v2/InferenceTest.java diff --git a/src/main/java/com/mindee/parsing/v2/InferenceFields.java b/src/main/java/com/mindee/parsing/v2/InferenceFields.java index 246da67f1..81058d70c 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceFields.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceFields.java @@ -1,8 +1,9 @@ package com.mindee.parsing.v2; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.mindee.parsing.SummaryHelper; import java.util.HashMap; -import java.util.Map; +import java.util.StringJoiner; import lombok.EqualsAndHashCode; /** @@ -13,10 +14,28 @@ public final class InferenceFields extends HashMap { @Override public String toString() { - StringBuilder strBuilder = new StringBuilder(); - for (Map.Entry entry : this.entrySet()) { - strBuilder.append(':').append(entry.getKey()).append(": ").append(entry.getValue()); + if (this.isEmpty()) { + return ""; } - return strBuilder.toString(); + StringJoiner joiner = new StringJoiner("\n"); + + this.forEach((fieldKey, fieldValue) -> { + StringBuilder strBuilder = new StringBuilder(); + strBuilder.append(':').append(fieldKey).append(": "); + + if (fieldValue.getListField() != null) { + ListField listField = fieldValue.getListField(); + if (listField.getItems() != null && !listField.getItems().isEmpty()) { + strBuilder.append(listField); + } + } else if (fieldValue.getObjectField() != null) { + strBuilder.append(fieldValue.getObjectField()); + } else if (fieldValue.getSimpleField() != null) { + strBuilder.append(fieldValue.getSimpleField().getValue() != null ? fieldValue.getSimpleField().getValue() : ""); + } + joiner.add(strBuilder); + }); + + return SummaryHelper.cleanSummary(joiner.toString()); } } diff --git a/src/main/java/com/mindee/parsing/v2/ObjectField.java b/src/main/java/com/mindee/parsing/v2/ObjectField.java index b0d641546..a0da14070 100644 --- a/src/main/java/com/mindee/parsing/v2/ObjectField.java +++ b/src/main/java/com/mindee/parsing/v2/ObjectField.java @@ -25,29 +25,7 @@ public class ObjectField extends BaseField { @Override public String toString() { - if (fields == null || fields.isEmpty()) { - return "\n"; - } - - StringBuilder outStr = new StringBuilder(); - - fields.forEach((fieldKey, fieldValue) -> { - outStr.append("\n"); - outStr.append(':').append(fieldKey).append(": "); - - if (fieldValue.getListField() != null) { - ListField listField = fieldValue.getListField(); - if (listField.getItems() != null && !listField.getItems().isEmpty()) { - outStr.append(listField); - } - } else if (fieldValue.getObjectField() != null) { - outStr.append(fieldValue.getObjectField()); - } else if (fieldValue.getSimpleField() != null) { - outStr.append(fieldValue.getSimpleField().getValue() != null ? fieldValue.getSimpleField().getValue() : ""); - } - }); - - return outStr + "\n"; + return "\n" + (fields != null ? fields.toString() : ""); } } diff --git a/src/main/java/com/mindee/parsing/v2/SimpleField.java b/src/main/java/com/mindee/parsing/v2/SimpleField.java index 88b5f1cda..90e1d7eb0 100644 --- a/src/main/java/com/mindee/parsing/v2/SimpleField.java +++ b/src/main/java/com/mindee/parsing/v2/SimpleField.java @@ -27,6 +27,6 @@ public final class SimpleField extends BaseField { @Override public String toString() { - return value == null ? "\n" : (value + "\n"); + return value == null ? "" : value.toString(); } } diff --git a/src/test/java/com/mindee/v2/InferenceTest.java b/src/test/java/com/mindee/v2/InferenceTest.java new file mode 100644 index 000000000..24c5e6fd6 --- /dev/null +++ b/src/test/java/com/mindee/v2/InferenceTest.java @@ -0,0 +1,138 @@ +package com.mindee.v2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mindee.parsing.v2.DynamicField; +import com.mindee.parsing.v2.DynamicField.FieldType; +import com.mindee.parsing.v2.InferenceFields; +import com.mindee.parsing.v2.InferenceResponse; +import com.mindee.parsing.v2.ListField; +import com.mindee.parsing.v2.ObjectField; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("InferenceV2 – field integrity checks") +class InferenceTest { + + /* ------------------------------------------------------------------ */ + /* Helper */ + /* ------------------------------------------------------------------ */ + + private static InferenceResponse loadPrediction(String name) throws IOException { + String resourcePath = "v2/products/financial_document/" + name + ".json"; + try (InputStream is = InferenceTest.class.getClassLoader().getResourceAsStream(resourcePath)) { + assertNotNull(is, "Test resource not found: " + resourcePath); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + return mapper.readValue(is, InferenceResponse.class); + } + } + + /* ------------------------------------------------------------------ */ + /* Tests – “blank” */ + /* ------------------------------------------------------------------ */ + + @Nested + @DisplayName("When the async prediction is blank") + class BlankPrediction { + + @Test + @DisplayName("all properties must be valid") + void asyncPredict_whenEmpty_mustHaveValidProperties() throws IOException { + InferenceResponse response = loadPrediction("blank"); + InferenceFields fields = response.getInference().getResult().getFields(); + + assertEquals(21, fields.size(), "Expected 21 fields"); + + /* taxes ----------------------------------------------------------------- */ + DynamicField taxes = fields.get("taxes"); + assertNotNull(taxes, "'taxes' field must exist"); + ListField taxesList = taxes.getListField(); + assertNotNull(taxesList, "'taxes' must be a ListField"); + assertTrue(taxesList.getItems().isEmpty(), "'taxes' list must be empty"); + + /* supplier_address ------------------------------------------------------- */ + DynamicField supplierAddress = fields.get("supplier_address"); + assertNotNull(supplierAddress, "'supplier_address' field must exist"); + ObjectField supplierObj = supplierAddress.getObjectField(); + assertNotNull(supplierObj, "'supplier_address' must be an ObjectField"); + + /* generic checks --------------------------------------------------------- */ + for (Map.Entry entry : fields.entrySet()) { + DynamicField value = entry.getValue(); + if (value == null) { + continue; + } + + FieldType type = value.getType(); + switch (type) { + case LIST_FIELD: + assertNotNull(value.getListField(), entry.getKey() + " – ListField expected"); + assertNull(value.getObjectField(), entry.getKey() + " – ObjectField must be null"); + assertNull(value.getSimpleField(), entry.getKey() + " – SimpleField must be null"); + break; + + case OBJECT_FIELD: + assertNotNull(value.getObjectField(), entry.getKey() + " – ObjectField expected"); + assertNull(value.getListField(), entry.getKey() + " – ListField must be null"); + assertNull(value.getSimpleField(), entry.getKey() + " – SimpleField must be null"); + break; + + default: // SimpleField (or any scalar) + assertNotNull(value.getSimpleField(), entry.getKey() + " – SimpleField expected"); + assertNull(value.getListField(), entry.getKey() + " – ListField must be null"); + assertNull(value.getObjectField(), entry.getKey() + " – ObjectField must be null"); + break; + } + } + } + } + + @Nested + @DisplayName("When the async prediction is complete") + class CompletePrediction { + + @Test + @DisplayName("all properties must be valid") + void asyncPredict_whenComplete_mustHaveValidProperties() throws IOException { + InferenceResponse response = loadPrediction("complete"); + InferenceFields fields = response.getInference().getResult().getFields(); + + assertEquals(21, fields.size(), "Expected 21 fields"); + + DynamicField taxes = fields.get("taxes"); + assertNotNull(taxes, "'taxes' field must exist"); + ListField taxesList = taxes.getListField(); + assertNotNull(taxesList, "'taxes' must be a ListField"); + assertEquals(1, taxesList.getItems().size(), "'taxes' list must contain exactly one item"); + assertNotNull(taxes.toString(), "'taxes' toString() must not be null"); + + ObjectField taxItemObj = taxesList.getItems().get(0).getObjectField(); + assertNotNull(taxItemObj, "First item of 'taxes' must be an ObjectField"); + assertEquals(3, taxItemObj.getFields().size(), "Tax ObjectField must contain 3 sub-fields"); + assertEquals( + 31.5, + taxItemObj.getFields().get("base").getSimpleField().getValue(), + "'taxes.base' value mismatch" + ); + + /* supplier_address ------------------------------------------------------- */ + DynamicField supplierAddress = fields.get("supplier_address"); + assertNotNull(supplierAddress, "'supplier_address' field must exist"); + + ObjectField supplierObj = supplierAddress.getObjectField(); + assertNotNull(supplierObj, "'supplier_address' must be an ObjectField"); + + DynamicField country = supplierObj.getFields().get("country"); + assertNotNull(country, "'supplier_address.country' must exist"); + assertEquals("USA", country.getSimpleField().getValue()); + assertEquals("USA", country.toString()); + + assertNotNull(supplierAddress.toString(), "'supplier_address'.toString() must not be null"); + } + } +} From df7ca94683a66f0ddbf5981e2acafa2efa4c01af Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Thu, 10 Jul 2025 09:58:02 +0200 Subject: [PATCH 07/12] add tests --- .../java/com/mindee/MindeeClientV2IT.java | 126 ++++++++++++++++++ .../java/com/mindee/MindeeClientV2Test.java | 104 +++++++++++++++ .../java/com/mindee/v2/InferenceTest.java | 35 ++--- 3 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 src/test/java/com/mindee/MindeeClientV2IT.java create mode 100644 src/test/java/com/mindee/MindeeClientV2Test.java diff --git a/src/test/java/com/mindee/MindeeClientV2IT.java b/src/test/java/com/mindee/MindeeClientV2IT.java new file mode 100644 index 000000000..f2e2c597e --- /dev/null +++ b/src/test/java/com/mindee/MindeeClientV2IT.java @@ -0,0 +1,126 @@ +package com.mindee; + +import com.mindee.InferencePredictOptions; +import com.mindee.MindeeClientV2; +import com.mindee.http.MindeeHttpExceptionV2; +import com.mindee.input.LocalInputSource; +import com.mindee.parsing.v2.InferenceResponse; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tag("integration") +@DisplayName("MindeeClientV2 – integration tests (V2)") +class MindeeClientV2IntegrationTest { + + private MindeeClientV2 mindeeClient; + private String modelId; + + @BeforeAll + void setUp() { + String apiKey = System.getenv("MINDEE_V2_API_KEY"); + modelId = System.getenv("MINDEE_V2_FINDOC_MODEL_ID"); + + assumeTrue( + apiKey != null && !apiKey.trim().isEmpty(), + "MINDEE_V2_API_KEY env var is missing – integration tests skipped" + ); + assumeTrue( + modelId != null && !modelId.trim().isEmpty(), + "MINDEE_V2_FINDOC_MODEL_ID env var is missing – integration tests skipped" + ); + + mindeeClient = new MindeeClientV2(apiKey); + } + + @Test + @DisplayName("Empty, multi-page PDF – enqueue & parse must succeed") + void parseFile_emptyMultiPage_mustSucceed() throws IOException, InterruptedException { + LocalInputSource source = new LocalInputSource( + new File("src/test/resources/file_types/pdf/multipage_cut-2.pdf")); + + InferencePredictOptions options = + InferencePredictOptions.builder(modelId).build(); + + InferenceResponse response = mindeeClient.enqueueAndParse(source, options); + + assertNotNull(response); + assertNotNull(response.getInference()); + + assertNotNull(response.getInference().getFile()); + assertEquals("multipage_cut-2.pdf", response.getInference().getFile().getName()); + + assertNotNull(response.getInference().getModel()); + assertEquals(modelId, response.getInference().getModel().getId()); + + assertNotNull(response.getInference().getResult()); + assertNull(response.getInference().getResult().getOptions()); + } + + @Test + @DisplayName("Filled, single-page image – enqueue & parse must succeed") + void parseFile_filledSinglePage_mustSucceed() throws IOException, InterruptedException { + LocalInputSource source = new LocalInputSource( + new File("src/test/resources/products/financial_document/default_sample.jpg")); + + InferencePredictOptions options = + InferencePredictOptions.builder(modelId).build(); + + InferenceResponse response = mindeeClient.enqueueAndParse(source, options); + + assertNotNull(response); + assertNotNull(response.getInference()); + + assertNotNull(response.getInference().getFile()); + assertEquals("default_sample.jpg", response.getInference().getFile().getName()); + + assertNotNull(response.getInference().getModel()); + assertEquals(modelId, response.getInference().getModel().getId()); + + assertNotNull(response.getInference().getResult()); + assertNotNull(response.getInference().getResult().getFields()); + assertNotNull(response.getInference().getResult().getFields().get("supplier_name")); + assertEquals( + "John Smith", + response.getInference() + .getResult() + .getFields() + .get("supplier_name") + .getSimpleField() + .getValue() + ); + } + + @Test + @DisplayName("Invalid model ID – enqueue must raise 422") + void invalidModel_mustThrowError() throws IOException { + LocalInputSource source = new LocalInputSource( + new File("src/test/resources/file_types/pdf/multipage_cut-2.pdf")); + + InferencePredictOptions options = + InferencePredictOptions.builder("INVALID MODEL ID").build(); + + MindeeHttpExceptionV2 ex = assertThrows( + MindeeHttpExceptionV2.class, + () -> mindeeClient.enqueue(source, options) + ); + assertEquals(422, ex.getStatus()); + } + + @Test + @DisplayName("Invalid job ID – parseQueued must raise an error") + void invalidJob_mustThrowError() { + MindeeHttpExceptionV2 ex = assertThrows( + MindeeHttpExceptionV2.class, + () -> mindeeClient.parseQueued("not-a-valid-job-ID") + ); + assertEquals(404, ex.getStatus()); + assertNotNull(ex); + } +} diff --git a/src/test/java/com/mindee/MindeeClientV2Test.java b/src/test/java/com/mindee/MindeeClientV2Test.java new file mode 100644 index 000000000..15eb5945b --- /dev/null +++ b/src/test/java/com/mindee/MindeeClientV2Test.java @@ -0,0 +1,104 @@ +package com.mindee; + +import com.mindee.http.MindeeApiV2; +import com.mindee.input.LocalInputSource; +import com.mindee.input.LocalResponse; +import com.mindee.parsing.v2.CommonResponse; +import com.mindee.parsing.v2.InferenceResponse; +import com.mindee.parsing.v2.JobResponse; +import java.io.File; +import java.io.IOException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@DisplayName("MindeeClientV2 – client / API interaction tests") +class MindeeClientV2Test { + /** + * Creates a fully mocked MindeeClientV2. + */ + private static MindeeClientV2 makeClientWithMockedApi(MindeeApiV2 mockedApi) { + return new MindeeClientV2(mockedApi); + } + + @Nested + @DisplayName("enqueue()") + class Enqueue { + @Test + @DisplayName("sends exactly one HTTP call and yields a non-null response") + void enqueue_post_async() throws IOException { + MindeeApiV2 predictable = Mockito.mock(MindeeApiV2.class); + when(predictable.enqueuePost(any(LocalInputSource.class), any(InferencePredictOptions.class))) + .thenReturn(new JobResponse()); + + MindeeClientV2 mindeeClient = makeClientWithMockedApi(predictable); + + LocalInputSource input = + new LocalInputSource(new File("src/test/resources/file_types/pdf/blank_1.pdf")); + JobResponse response = mindeeClient.enqueue( + input, + InferencePredictOptions.builder("dummy-model-id").build() + ); + + assertNotNull(response, "enqueue() must return a response"); + verify(predictable, atMostOnce()) + .enqueuePost(any(LocalInputSource.class), any(InferencePredictOptions.class)); + } + } + + @Nested + @DisplayName("parseQueued()") + class ParseQueued { + @Test + @DisplayName("hits the HTTP endpoint once and returns a non-null response") + void document_getQueued_async() { + MindeeApiV2 predictable = Mockito.mock(MindeeApiV2.class); + when(predictable.getInferenceFromQueue(anyString())) + .thenReturn(new JobResponse()); + + MindeeClientV2 mindeeClient = makeClientWithMockedApi(predictable); + + CommonResponse response = mindeeClient.parseQueued("dummy-id"); + assertNotNull(response, "parseQueued() must return a response"); + verify(predictable, atMostOnce()).getInferenceFromQueue(anyString()); + } + } + + @Nested + @DisplayName("loadInference()") + class LoadInference { + + @Test + @DisplayName("parses local JSON and exposes correct field values") + void inference_loadsLocally() throws IOException { + MindeeClientV2 mindeeClient = new MindeeClientV2("dummy"); + File jsonFile = + new File("src/test/resources/v2/products/financial_document/complete.json"); + LocalResponse localResponse = new LocalResponse(jsonFile); + + InferenceResponse loaded = mindeeClient.loadInference(localResponse); + + assertNotNull(loaded, "Loaded InferenceResponse must not be null"); + assertEquals( + "12345678-1234-1234-1234-123456789abc", + loaded.getInference().getModel().getId(), + "Model Id mismatch" + ); + assertEquals( + "John Smith", + loaded.getInference() + .getResult() + .getFields() + .get("supplier_name") + .getSimpleField() + .getValue(), + "Supplier name mismatch" + ); + } + } +} diff --git a/src/test/java/com/mindee/v2/InferenceTest.java b/src/test/java/com/mindee/v2/InferenceTest.java index 24c5e6fd6..b47363070 100644 --- a/src/test/java/com/mindee/v2/InferenceTest.java +++ b/src/test/java/com/mindee/v2/InferenceTest.java @@ -1,12 +1,16 @@ package com.mindee.v2; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mindee.MindeeClientV2; +import com.mindee.input.LocalResponse; import com.mindee.parsing.v2.DynamicField; import com.mindee.parsing.v2.DynamicField.FieldType; import com.mindee.parsing.v2.InferenceFields; import com.mindee.parsing.v2.InferenceResponse; import com.mindee.parsing.v2.ListField; import com.mindee.parsing.v2.ObjectField; + +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Map; @@ -18,24 +22,6 @@ @DisplayName("InferenceV2 – field integrity checks") class InferenceTest { - - /* ------------------------------------------------------------------ */ - /* Helper */ - /* ------------------------------------------------------------------ */ - - private static InferenceResponse loadPrediction(String name) throws IOException { - String resourcePath = "v2/products/financial_document/" + name + ".json"; - try (InputStream is = InferenceTest.class.getClassLoader().getResourceAsStream(resourcePath)) { - assertNotNull(is, "Test resource not found: " + resourcePath); - ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); - return mapper.readValue(is, InferenceResponse.class); - } - } - - /* ------------------------------------------------------------------ */ - /* Tests – “blank” */ - /* ------------------------------------------------------------------ */ - @Nested @DisplayName("When the async prediction is blank") class BlankPrediction { @@ -43,25 +29,23 @@ class BlankPrediction { @Test @DisplayName("all properties must be valid") void asyncPredict_whenEmpty_mustHaveValidProperties() throws IOException { - InferenceResponse response = loadPrediction("blank"); + MindeeClientV2 mindeeClient = new MindeeClientV2("dummy"); + InferenceResponse response = mindeeClient.loadInference(new LocalResponse(InferenceTest.class.getClassLoader().getResourceAsStream("v2/products/financial_document/blank.json"))); InferenceFields fields = response.getInference().getResult().getFields(); assertEquals(21, fields.size(), "Expected 21 fields"); - /* taxes ----------------------------------------------------------------- */ DynamicField taxes = fields.get("taxes"); assertNotNull(taxes, "'taxes' field must exist"); ListField taxesList = taxes.getListField(); assertNotNull(taxesList, "'taxes' must be a ListField"); assertTrue(taxesList.getItems().isEmpty(), "'taxes' list must be empty"); - /* supplier_address ------------------------------------------------------- */ DynamicField supplierAddress = fields.get("supplier_address"); assertNotNull(supplierAddress, "'supplier_address' field must exist"); ObjectField supplierObj = supplierAddress.getObjectField(); assertNotNull(supplierObj, "'supplier_address' must be an ObjectField"); - /* generic checks --------------------------------------------------------- */ for (Map.Entry entry : fields.entrySet()) { DynamicField value = entry.getValue(); if (value == null) { @@ -82,7 +66,8 @@ void asyncPredict_whenEmpty_mustHaveValidProperties() throws IOException { assertNull(value.getSimpleField(), entry.getKey() + " – SimpleField must be null"); break; - default: // SimpleField (or any scalar) + case SIMPLE_FIELD: + default: assertNotNull(value.getSimpleField(), entry.getKey() + " – SimpleField expected"); assertNull(value.getListField(), entry.getKey() + " – ListField must be null"); assertNull(value.getObjectField(), entry.getKey() + " – ObjectField must be null"); @@ -99,7 +84,8 @@ class CompletePrediction { @Test @DisplayName("all properties must be valid") void asyncPredict_whenComplete_mustHaveValidProperties() throws IOException { - InferenceResponse response = loadPrediction("complete"); + MindeeClientV2 mindeeClient = new MindeeClientV2("dummy"); + InferenceResponse response = mindeeClient.loadInference(new LocalResponse(InferenceTest.class.getClassLoader().getResourceAsStream("v2/products/financial_document/complete.json"))); InferenceFields fields = response.getInference().getResult().getFields(); assertEquals(21, fields.size(), "Expected 21 fields"); @@ -120,7 +106,6 @@ void asyncPredict_whenComplete_mustHaveValidProperties() throws IOException { "'taxes.base' value mismatch" ); - /* supplier_address ------------------------------------------------------- */ DynamicField supplierAddress = fields.get("supplier_address"); assertNotNull(supplierAddress, "'supplier_address' field must exist"); From 4c00a35baa30e28de243702dcb3f81a95423cd2e Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:00:06 +0200 Subject: [PATCH 08/12] fix permissions for workflows --- .github/workflows/pull-request.yml | 3 +++ src/test/java/com/mindee/MindeeClientV2IT.java | 5 ----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f91b8bf91..5b7e34b22 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -17,6 +17,9 @@ jobs: codeql: uses: ./.github/workflows/_codeql.yml needs: build + permissions: + actions: read + security-events: write test_integrations: uses: ./.github/workflows/_test-integrations.yml needs: build diff --git a/src/test/java/com/mindee/MindeeClientV2IT.java b/src/test/java/com/mindee/MindeeClientV2IT.java index f2e2c597e..427f8a6cf 100644 --- a/src/test/java/com/mindee/MindeeClientV2IT.java +++ b/src/test/java/com/mindee/MindeeClientV2IT.java @@ -1,16 +1,11 @@ package com.mindee; -import com.mindee.InferencePredictOptions; -import com.mindee.MindeeClientV2; import com.mindee.http.MindeeHttpExceptionV2; import com.mindee.input.LocalInputSource; import com.mindee.parsing.v2.InferenceResponse; - import java.io.File; import java.io.IOException; - import org.junit.jupiter.api.*; - import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; From dc8e25ea262dfc2cb2e0bf372e5716f386f2d3cd Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:00:49 +0200 Subject: [PATCH 09/12] fix perms again --- .github/workflows/pull-request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5b7e34b22..8c536432d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -18,6 +18,7 @@ jobs: uses: ./.github/workflows/_codeql.yml needs: build permissions: + contents: read actions: read security-events: write test_integrations: From 40e3e9c06d728c4a42b33dc54f03eddc62e3b3b1 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:29:47 +0200 Subject: [PATCH 10/12] bump test lib, fix tests, add missing object --- .../java/com/mindee/parsing/v2/Inference.java | 5 + .../mindee/parsing/v2/InferenceOptions.java | 4 +- .../java/com/mindee/parsing/v2/RawText.java | 24 +++ .../java/com/mindee/v2/InferenceTest.java | 141 ++++++++++++++++-- src/test/resources | 2 +- 5 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/mindee/parsing/v2/RawText.java diff --git a/src/main/java/com/mindee/parsing/v2/Inference.java b/src/main/java/com/mindee/parsing/v2/Inference.java index e132afc14..51b3ddcee 100644 --- a/src/main/java/com/mindee/parsing/v2/Inference.java +++ b/src/main/java/com/mindee/parsing/v2/Inference.java @@ -17,6 +17,11 @@ @AllArgsConstructor @NoArgsConstructor public class Inference { + /** + * Inference ID. + */ + @JsonProperty("id") + private String id; /** * Model info. diff --git a/src/main/java/com/mindee/parsing/v2/InferenceOptions.java b/src/main/java/com/mindee/parsing/v2/InferenceOptions.java index 92fb5351e..2411c9175 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceOptions.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceOptions.java @@ -10,6 +10,6 @@ @Getter public final class InferenceOptions { - @JsonProperty("raw_text") - private List rawText; + @JsonProperty("raw_texts") + private List rawTexts; } diff --git a/src/main/java/com/mindee/parsing/v2/RawText.java b/src/main/java/com/mindee/parsing/v2/RawText.java new file mode 100644 index 000000000..b265003b3 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/RawText.java @@ -0,0 +1,24 @@ +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; + +/** + * Raw text as found in the document. + */ +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class RawText { + + @JsonProperty("page") + private Integer page; + + @JsonProperty("content") + private String content; +} diff --git a/src/test/java/com/mindee/v2/InferenceTest.java b/src/test/java/com/mindee/v2/InferenceTest.java index b47363070..9bd4d41b4 100644 --- a/src/test/java/com/mindee/v2/InferenceTest.java +++ b/src/test/java/com/mindee/v2/InferenceTest.java @@ -1,18 +1,11 @@ package com.mindee.v2; -import com.fasterxml.jackson.databind.ObjectMapper; import com.mindee.MindeeClientV2; import com.mindee.input.LocalResponse; -import com.mindee.parsing.v2.DynamicField; +import com.mindee.parsing.v2.*; import com.mindee.parsing.v2.DynamicField.FieldType; -import com.mindee.parsing.v2.InferenceFields; -import com.mindee.parsing.v2.InferenceResponse; -import com.mindee.parsing.v2.ListField; -import com.mindee.parsing.v2.ObjectField; - -import java.io.File; import java.io.IOException; -import java.io.InputStream; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -22,6 +15,13 @@ @DisplayName("InferenceV2 – field integrity checks") class InferenceTest { + + private InferenceResponse loadFromResource(String resourcePath) throws IOException { + MindeeClientV2 dummyClient = new MindeeClientV2("dummy"); + return dummyClient.loadInference(new LocalResponse(InferenceTest.class.getClassLoader().getResourceAsStream(resourcePath))); + } + + @Nested @DisplayName("When the async prediction is blank") class BlankPrediction { @@ -29,8 +29,7 @@ class BlankPrediction { @Test @DisplayName("all properties must be valid") void asyncPredict_whenEmpty_mustHaveValidProperties() throws IOException { - MindeeClientV2 mindeeClient = new MindeeClientV2("dummy"); - InferenceResponse response = mindeeClient.loadInference(new LocalResponse(InferenceTest.class.getClassLoader().getResourceAsStream("v2/products/financial_document/blank.json"))); + InferenceResponse response = loadFromResource("v2/products/financial_document/blank.json"); InferenceFields fields = response.getInference().getResult().getFields(); assertEquals(21, fields.size(), "Expected 21 fields"); @@ -84,8 +83,7 @@ class CompletePrediction { @Test @DisplayName("all properties must be valid") void asyncPredict_whenComplete_mustHaveValidProperties() throws IOException { - MindeeClientV2 mindeeClient = new MindeeClientV2("dummy"); - InferenceResponse response = mindeeClient.loadInference(new LocalResponse(InferenceTest.class.getClassLoader().getResourceAsStream("v2/products/financial_document/complete.json"))); + InferenceResponse response = loadFromResource("v2/products/financial_document/complete.json"); InferenceFields fields = response.getInference().getResult().getFields(); assertEquals(21, fields.size(), "Expected 21 fields"); @@ -120,4 +118,121 @@ void asyncPredict_whenComplete_mustHaveValidProperties() throws IOException { assertNotNull(supplierAddress.toString(), "'supplier_address'.toString() must not be null"); } } + + @Nested + @DisplayName("deep_nested_fields.json") + class DeepNestedFields { + + @Test + @DisplayName("all nested structures must be typed correctly") + void deepNestedFields_mustExposeCorrectTypes() throws IOException { + InferenceResponse resp = loadFromResource("v2/inference/deep_nested_fields.json"); + Inference inf = resp.getInference(); + assertNotNull(inf); + + InferenceFields root = inf.getResult().getFields(); + assertNotNull(root.get("field_simple").getSimpleField()); + assertNotNull(root.get("field_object").getObjectField()); + + ObjectField fieldObject = root.get("field_object").getObjectField(); + InferenceFields lvl1 = fieldObject.getFields(); + assertNotNull(lvl1.get("sub_object_list").getListField()); + assertNotNull(lvl1.get("sub_object_object").getObjectField()); + + ObjectField subObjectObject = lvl1.get("sub_object_object").getObjectField(); + InferenceFields lvl2 = subObjectObject.getFields(); + assertNotNull(lvl2.get("sub_object_object_sub_object_list").getListField()); + + ListField nestedList = lvl2.get("sub_object_object_sub_object_list").getListField(); + List items = nestedList.getItems(); + assertFalse(items.isEmpty()); + assertNotNull(items.get(0).getObjectField()); + + ObjectField firstItem = items.get(0).getObjectField(); + SimpleField deepSimple = firstItem.getFields() + .get("sub_object_object_sub_object_list_simple").getSimpleField(); + assertEquals("value_9", deepSimple.getValue()); + } + } + + @Nested + @DisplayName("standard_field_types.json") + class StandardFieldTypes { + + @Test + @DisplayName("simple / object / list variants must be recognised") + void standardFieldTypes_mustExposeCorrectTypes() throws IOException { + InferenceResponse resp = loadFromResource("v2/inference/standard_field_types.json"); + Inference inf = resp.getInference(); + assertNotNull(inf); + + InferenceFields root = inf.getResult().getFields(); + assertNotNull(root.get("field_simple").getSimpleField()); + assertNotNull(root.get("field_object").getObjectField()); + assertNotNull(root.get("field_simple_list").getListField()); + assertNotNull(root.get("field_object_list").getListField()); + } + } + + @Nested + @DisplayName("raw_texts.json") + class RawTexts { + + @Test + @DisplayName("raw texts option must be parsed and exposed") + void rawTexts_mustBeAccessible() throws IOException { + InferenceResponse resp = loadFromResource("v2/inference/raw_texts.json"); + Inference inf = resp.getInference(); + assertNotNull(inf); + + InferenceOptions opts = inf.getResult().getOptions(); + assertNotNull(opts, "Options should not be null"); + + List rawTexts = opts.getRawTexts(); + assertEquals(2, rawTexts.size()); + + RawText first = rawTexts.get(0); + assertEquals(0, first.getPage()); + assertEquals("This is the raw text of the first page...", first.getContent()); + } + } + + @Nested + @DisplayName("complete.json – full inference response") + class FullInference { + @Test + @DisplayName("complete financial-document JSON must round-trip correctly") + void fullInferenceResponse_mustExposeEveryProperty() throws IOException { + InferenceResponse resp = loadFromResource("v2/products/financial_document/complete.json"); + + Inference inf = resp.getInference(); + assertNotNull(inf); + assertEquals("12345678-1234-1234-1234-123456789abc", inf.getId()); + + InferenceFields f = inf.getResult().getFields(); + + SimpleField date = f.get("date").getSimpleField(); + assertEquals("2019-11-02", date.getValue()); + + ListField taxes = f.get("taxes").getListField(); + ObjectField firstTax = taxes.getItems().get(0).getObjectField(); + SimpleField baseTax = firstTax.getFields().get("base").getSimpleField(); + assertEquals(31.5, baseTax.getValue()); + + ObjectField customerAddr = f.get("customer_address").getObjectField(); + SimpleField city = customerAddr.getFields().get("city").getSimpleField(); + assertEquals("New York", city.getValue()); + + InferenceModel model = inf.getModel(); + assertNotNull(model); + assertEquals("12345678-1234-1234-1234-123456789abc", model.getId()); + + InferenceFile file = inf.getFile(); + assertNotNull(file); + assertEquals("complete.jpg", file.getName()); + assertNull(file.getAlias()); + + assertNull(inf.getResult().getOptions()); + } + } } diff --git a/src/test/resources b/src/test/resources index f599a960e..e2912fbd3 160000 --- a/src/test/resources +++ b/src/test/resources @@ -1 +1 @@ -Subproject commit f599a960e78f4a390984c6263f387aa8cdebe0f0 +Subproject commit e2912fbd362b7ccf595a5a8d6cc6a67f78901cde From 35f30cffa2386687c3fd156339c14d0ae7e3ec2b Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:15:32 +0200 Subject: [PATCH 11/12] refactor v2 internals --- src/main/java/com/mindee/InferencePredictOptions.java | 1 - src/main/java/com/mindee/parsing/v2/Inference.java | 4 ++-- .../java/com/mindee/parsing/v2/InferenceResult.java | 3 ++- .../{InferenceFile.java => InferenceResultFile.java} | 2 +- .../{InferenceModel.java => InferenceResultModel.java} | 2 +- ...ferenceOptions.java => InferenceResultOptions.java} | 2 +- src/main/java/com/mindee/parsing/v2/Job.java | 2 +- .../v2/{Webhook.java => JobResponseWebhook.java} | 4 ++-- src/main/java/com/mindee/parsing/v2/RawText.java | 8 ++++++-- .../com/mindee/parsing/v2/{ => field}/BaseField.java | 2 +- .../mindee/parsing/v2/{ => field}/DynamicField.java | 2 +- .../v2/{ => field}/DynamicFieldDeserializer.java | 2 +- .../mindee/parsing/v2/{ => field}/InferenceFields.java | 2 +- .../com/mindee/parsing/v2/{ => field}/ListField.java | 2 +- .../com/mindee/parsing/v2/{ => field}/ObjectField.java | 2 +- .../com/mindee/parsing/v2/{ => field}/SimpleField.java | 2 +- .../v2/{ => field}/SimpleFieldDeserializer.java | 2 +- src/test/java/com/mindee/v2/InferenceTest.java | 10 ++++++---- 18 files changed, 30 insertions(+), 24 deletions(-) rename src/main/java/com/mindee/parsing/v2/{InferenceFile.java => InferenceResultFile.java} (94%) rename src/main/java/com/mindee/parsing/v2/{InferenceModel.java => InferenceResultModel.java} (93%) rename src/main/java/com/mindee/parsing/v2/{InferenceOptions.java => InferenceResultOptions.java} (85%) rename src/main/java/com/mindee/parsing/v2/{Webhook.java => JobResponseWebhook.java} (93%) rename src/main/java/com/mindee/parsing/v2/{ => field}/BaseField.java (66%) rename src/main/java/com/mindee/parsing/v2/{ => field}/DynamicField.java (97%) rename src/main/java/com/mindee/parsing/v2/{ => field}/DynamicFieldDeserializer.java (97%) rename src/main/java/com/mindee/parsing/v2/{ => field}/InferenceFields.java (97%) rename src/main/java/com/mindee/parsing/v2/{ => field}/ListField.java (96%) rename src/main/java/com/mindee/parsing/v2/{ => field}/ObjectField.java (94%) rename src/main/java/com/mindee/parsing/v2/{ => field}/SimpleField.java (95%) rename src/main/java/com/mindee/parsing/v2/{ => field}/SimpleFieldDeserializer.java (96%) diff --git a/src/main/java/com/mindee/InferencePredictOptions.java b/src/main/java/com/mindee/InferencePredictOptions.java index 667ec1aa7..2653369aa 100644 --- a/src/main/java/com/mindee/InferencePredictOptions.java +++ b/src/main/java/com/mindee/InferencePredictOptions.java @@ -13,7 +13,6 @@ @Getter @Data public final class InferencePredictOptions { - /** * ID of the model (required). */ diff --git a/src/main/java/com/mindee/parsing/v2/Inference.java b/src/main/java/com/mindee/parsing/v2/Inference.java index 51b3ddcee..2e7d274bf 100644 --- a/src/main/java/com/mindee/parsing/v2/Inference.java +++ b/src/main/java/com/mindee/parsing/v2/Inference.java @@ -27,13 +27,13 @@ public class Inference { * Model info. */ @JsonProperty("model") - private InferenceModel model; + private InferenceResultModel model; /** * File info. */ @JsonProperty("file") - private InferenceFile file; + private InferenceResultFile file; /** * Model result values. diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResult.java b/src/main/java/com/mindee/parsing/v2/InferenceResult.java index 81013c889..4b6984b37 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceResult.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResult.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.mindee.parsing.v2.field.InferenceFields; import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -28,7 +29,7 @@ public final class InferenceResult { * Options. */ @JsonProperty("options") - private InferenceOptions options; + private InferenceResultOptions options; @Override public String toString() { diff --git a/src/main/java/com/mindee/parsing/v2/InferenceFile.java b/src/main/java/com/mindee/parsing/v2/InferenceResultFile.java similarity index 94% rename from src/main/java/com/mindee/parsing/v2/InferenceFile.java rename to src/main/java/com/mindee/parsing/v2/InferenceResultFile.java index 3f7c8d8a1..f7f424d69 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceFile.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResultFile.java @@ -15,7 +15,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @AllArgsConstructor @NoArgsConstructor -public class InferenceFile { +public class InferenceResultFile { /** * File name. */ diff --git a/src/main/java/com/mindee/parsing/v2/InferenceModel.java b/src/main/java/com/mindee/parsing/v2/InferenceResultModel.java similarity index 93% rename from src/main/java/com/mindee/parsing/v2/InferenceModel.java rename to src/main/java/com/mindee/parsing/v2/InferenceResultModel.java index 02c24f74f..70cf0e80d 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceModel.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResultModel.java @@ -15,7 +15,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @AllArgsConstructor @NoArgsConstructor -public class InferenceModel { +public class InferenceResultModel { /** * The ID of the model. diff --git a/src/main/java/com/mindee/parsing/v2/InferenceOptions.java b/src/main/java/com/mindee/parsing/v2/InferenceResultOptions.java similarity index 85% rename from src/main/java/com/mindee/parsing/v2/InferenceOptions.java rename to src/main/java/com/mindee/parsing/v2/InferenceResultOptions.java index 2411c9175..80d4cc349 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceOptions.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResultOptions.java @@ -8,7 +8,7 @@ * Option response for V2 API inference. */ @Getter -public final class InferenceOptions { +public final class InferenceResultOptions { @JsonProperty("raw_texts") private List rawTexts; diff --git a/src/main/java/com/mindee/parsing/v2/Job.java b/src/main/java/com/mindee/parsing/v2/Job.java index a96d01f60..d08f05e23 100644 --- a/src/main/java/com/mindee/parsing/v2/Job.java +++ b/src/main/java/com/mindee/parsing/v2/Job.java @@ -80,5 +80,5 @@ public final class Job { * Polling URL. */ @JsonProperty("webhooks") - private List webhooks; + private List webhooks; } diff --git a/src/main/java/com/mindee/parsing/v2/Webhook.java b/src/main/java/com/mindee/parsing/v2/JobResponseWebhook.java similarity index 93% rename from src/main/java/com/mindee/parsing/v2/Webhook.java rename to src/main/java/com/mindee/parsing/v2/JobResponseWebhook.java index c58af38ec..09ca9f123 100644 --- a/src/main/java/com/mindee/parsing/v2/Webhook.java +++ b/src/main/java/com/mindee/parsing/v2/JobResponseWebhook.java @@ -11,14 +11,14 @@ import lombok.NoArgsConstructor; /** - * Webhook info. + * JobResponseWebhook info. */ @Getter @EqualsAndHashCode @JsonIgnoreProperties(ignoreUnknown = true) @AllArgsConstructor @NoArgsConstructor -public final class Webhook { +public final class JobResponseWebhook { /** * ID of the webhook. diff --git a/src/main/java/com/mindee/parsing/v2/RawText.java b/src/main/java/com/mindee/parsing/v2/RawText.java index b265003b3..3fe0a5140 100644 --- a/src/main/java/com/mindee/parsing/v2/RawText.java +++ b/src/main/java/com/mindee/parsing/v2/RawText.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -15,10 +14,15 @@ @AllArgsConstructor @NoArgsConstructor public class RawText { - + /* + * Page Number the text was found on. + */ @JsonProperty("page") private Integer page; + /* + * Content of the raw text. + */ @JsonProperty("content") private String content; } diff --git a/src/main/java/com/mindee/parsing/v2/BaseField.java b/src/main/java/com/mindee/parsing/v2/field/BaseField.java similarity index 66% rename from src/main/java/com/mindee/parsing/v2/BaseField.java rename to src/main/java/com/mindee/parsing/v2/field/BaseField.java index fc2081d24..aef339840 100644 --- a/src/main/java/com/mindee/parsing/v2/BaseField.java +++ b/src/main/java/com/mindee/parsing/v2/field/BaseField.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; /** * Base class for V2 fields. diff --git a/src/main/java/com/mindee/parsing/v2/DynamicField.java b/src/main/java/com/mindee/parsing/v2/field/DynamicField.java similarity index 97% rename from src/main/java/com/mindee/parsing/v2/DynamicField.java rename to src/main/java/com/mindee/parsing/v2/field/DynamicField.java index 1e26b0fc8..a794332b9 100644 --- a/src/main/java/com/mindee/parsing/v2/DynamicField.java +++ b/src/main/java/com/mindee/parsing/v2/field/DynamicField.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java b/src/main/java/com/mindee/parsing/v2/field/DynamicFieldDeserializer.java similarity index 97% rename from src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java rename to src/main/java/com/mindee/parsing/v2/field/DynamicFieldDeserializer.java index dbec81b22..4d933aabd 100644 --- a/src/main/java/com/mindee/parsing/v2/DynamicFieldDeserializer.java +++ b/src/main/java/com/mindee/parsing/v2/field/DynamicFieldDeserializer.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; diff --git a/src/main/java/com/mindee/parsing/v2/InferenceFields.java b/src/main/java/com/mindee/parsing/v2/field/InferenceFields.java similarity index 97% rename from src/main/java/com/mindee/parsing/v2/InferenceFields.java rename to src/main/java/com/mindee/parsing/v2/field/InferenceFields.java index 81058d70c..3f17c2600 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceFields.java +++ b/src/main/java/com/mindee/parsing/v2/field/InferenceFields.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.mindee.parsing.SummaryHelper; diff --git a/src/main/java/com/mindee/parsing/v2/ListField.java b/src/main/java/com/mindee/parsing/v2/field/ListField.java similarity index 96% rename from src/main/java/com/mindee/parsing/v2/ListField.java rename to src/main/java/com/mindee/parsing/v2/field/ListField.java index 0d981154b..16bf49e27 100644 --- a/src/main/java/com/mindee/parsing/v2/ListField.java +++ b/src/main/java/com/mindee/parsing/v2/field/ListField.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/mindee/parsing/v2/ObjectField.java b/src/main/java/com/mindee/parsing/v2/field/ObjectField.java similarity index 94% rename from src/main/java/com/mindee/parsing/v2/ObjectField.java rename to src/main/java/com/mindee/parsing/v2/field/ObjectField.java index a0da14070..90c44a5de 100644 --- a/src/main/java/com/mindee/parsing/v2/ObjectField.java +++ b/src/main/java/com/mindee/parsing/v2/field/ObjectField.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/mindee/parsing/v2/SimpleField.java b/src/main/java/com/mindee/parsing/v2/field/SimpleField.java similarity index 95% rename from src/main/java/com/mindee/parsing/v2/SimpleField.java rename to src/main/java/com/mindee/parsing/v2/field/SimpleField.java index 90e1d7eb0..20dd783a8 100644 --- a/src/main/java/com/mindee/parsing/v2/SimpleField.java +++ b/src/main/java/com/mindee/parsing/v2/field/SimpleField.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/mindee/parsing/v2/SimpleFieldDeserializer.java b/src/main/java/com/mindee/parsing/v2/field/SimpleFieldDeserializer.java similarity index 96% rename from src/main/java/com/mindee/parsing/v2/SimpleFieldDeserializer.java rename to src/main/java/com/mindee/parsing/v2/field/SimpleFieldDeserializer.java index 049189db0..56de1c508 100644 --- a/src/main/java/com/mindee/parsing/v2/SimpleFieldDeserializer.java +++ b/src/main/java/com/mindee/parsing/v2/field/SimpleFieldDeserializer.java @@ -1,4 +1,4 @@ -package com.mindee.parsing.v2; +package com.mindee.parsing.v2.field; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; diff --git a/src/test/java/com/mindee/v2/InferenceTest.java b/src/test/java/com/mindee/v2/InferenceTest.java index 9bd4d41b4..dea0c9dbf 100644 --- a/src/test/java/com/mindee/v2/InferenceTest.java +++ b/src/test/java/com/mindee/v2/InferenceTest.java @@ -3,10 +3,12 @@ import com.mindee.MindeeClientV2; import com.mindee.input.LocalResponse; import com.mindee.parsing.v2.*; -import com.mindee.parsing.v2.DynamicField.FieldType; +import com.mindee.parsing.v2.field.*; +import com.mindee.parsing.v2.field.DynamicField.FieldType; import java.io.IOException; import java.util.List; import java.util.Map; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -185,7 +187,7 @@ void rawTexts_mustBeAccessible() throws IOException { Inference inf = resp.getInference(); assertNotNull(inf); - InferenceOptions opts = inf.getResult().getOptions(); + InferenceResultOptions opts = inf.getResult().getOptions(); assertNotNull(opts, "Options should not be null"); List rawTexts = opts.getRawTexts(); @@ -223,11 +225,11 @@ void fullInferenceResponse_mustExposeEveryProperty() throws IOException { SimpleField city = customerAddr.getFields().get("city").getSimpleField(); assertEquals("New York", city.getValue()); - InferenceModel model = inf.getModel(); + InferenceResultModel model = inf.getModel(); assertNotNull(model); assertEquals("12345678-1234-1234-1234-123456789abc", model.getId()); - InferenceFile file = inf.getFile(); + InferenceResultFile file = inf.getFile(); assertNotNull(file); assertEquals("complete.jpg", file.getName()); assertNull(file.getAlias()); From 067d534ebde2d7be4b6c231cb7b41beb2d075bbf Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:55:44 +0200 Subject: [PATCH 12/12] apply fixes to match latest test syntaxes --- docs/code_samples/default_v2.txt | 4 +- ...dictOptions.java => InferenceOptions.java} | 10 ++--- src/main/java/com/mindee/MindeeClientV2.java | 4 +- .../java/com/mindee/http/MindeeApiV2.java | 4 +- .../java/com/mindee/http/MindeeHttpApiV2.java | 8 ++-- .../java/com/mindee/parsing/v2/Inference.java | 19 +++++----- .../mindee/parsing/v2/InferenceResult.java | 8 +++- .../parsing/v2/InferenceResultFile.java | 4 ++ .../mindee/parsing/v2/field/BaseField.java | 13 +++++++ .../parsing/v2/field/FieldConfidence.java | 38 +++++++++++++++++++ .../parsing/v2/field/FieldLocation.java | 35 +++++++++++++++++ .../parsing/v2/field/InferenceFields.java | 16 +++++--- .../mindee/parsing/v2/field/ListField.java | 16 ++++++-- .../mindee/parsing/v2/field/ObjectField.java | 5 ++- .../java/com/mindee/MindeeClientV2IT.java | 12 +++--- .../java/com/mindee/MindeeClientV2Test.java | 6 +-- .../{ => parsing}/v2/InferenceTest.java | 31 +++++++++++++-- src/test/resources | 2 +- 18 files changed, 187 insertions(+), 48 deletions(-) rename src/main/java/com/mindee/{InferencePredictOptions.java => InferenceOptions.java} (91%) create mode 100644 src/main/java/com/mindee/parsing/v2/field/FieldConfidence.java create mode 100644 src/main/java/com/mindee/parsing/v2/field/FieldLocation.java rename src/test/java/com/mindee/{ => parsing}/v2/InferenceTest.java (90%) diff --git a/docs/code_samples/default_v2.txt b/docs/code_samples/default_v2.txt index 3aff5904c..221430d2c 100644 --- a/docs/code_samples/default_v2.txt +++ b/docs/code_samples/default_v2.txt @@ -1,5 +1,5 @@ import com.mindee.MindeeClientV2; -import com.mindee.InferencePredictOptions; +import com.mindee.InferenceOptions; import com.mindee.input.LocalInputSource; import com.mindee.parsing.v2.InferenceResponse; import java.io.File; @@ -19,7 +19,7 @@ public class SimpleMindeeClient { // Prepare the enqueueing options // Note: modelId is mandatory. - InferencePredictOptions options = InferencePredictOptions.builder("MY_MODEL_ID").build(); + InferenceOptions options = InferenceOptions.builder("MY_MODEL_ID").build(); // Parse the file InferenceResponse response = mindeeClient.enqueueAndParse( diff --git a/src/main/java/com/mindee/InferencePredictOptions.java b/src/main/java/com/mindee/InferenceOptions.java similarity index 91% rename from src/main/java/com/mindee/InferencePredictOptions.java rename to src/main/java/com/mindee/InferenceOptions.java index 2653369aa..71307f479 100644 --- a/src/main/java/com/mindee/InferencePredictOptions.java +++ b/src/main/java/com/mindee/InferenceOptions.java @@ -12,7 +12,7 @@ */ @Getter @Data -public final class InferencePredictOptions { +public final class InferenceOptions { /** * ID of the model (required). */ @@ -54,7 +54,7 @@ public static Builder builder(String modelId) { } /** - * Fluent builder for {@link InferencePredictOptions}. + * Fluent builder for {@link InferenceOptions}. */ public static final class Builder { @@ -106,9 +106,9 @@ public Builder pollingOptions(AsyncPollingOptions pollingOptions) { return this; } - /** Build an immutable {@link InferencePredictOptions} instance. */ - public InferencePredictOptions build() { - return new InferencePredictOptions( + /** Build an immutable {@link InferenceOptions} instance. */ + public InferenceOptions build() { + return new InferenceOptions( modelId, fullText, rag, diff --git a/src/main/java/com/mindee/MindeeClientV2.java b/src/main/java/com/mindee/MindeeClientV2.java index 94ef7dbdf..c42cd35ca 100644 --- a/src/main/java/com/mindee/MindeeClientV2.java +++ b/src/main/java/com/mindee/MindeeClientV2.java @@ -44,7 +44,7 @@ public MindeeClientV2(PdfOperation pdfOperation, MindeeApiV2 mindeeApi) { */ public JobResponse enqueue( LocalInputSource inputSource, - InferencePredictOptions options) throws IOException { + InferenceOptions options) throws IOException { LocalInputSource finalInput; if (options.getPageOptions() != null) { finalInput = new LocalInputSource(getSplitFile(inputSource, options.getPageOptions()), inputSource.getFilename()); @@ -74,7 +74,7 @@ public CommonResponse parseQueued(String jobId) { */ public InferenceResponse enqueueAndParse( LocalInputSource inputSource, - InferencePredictOptions options) throws IOException, InterruptedException { + InferenceOptions options) throws IOException, InterruptedException { validatePollingOptions(options.getPollingOptions()); diff --git a/src/main/java/com/mindee/http/MindeeApiV2.java b/src/main/java/com/mindee/http/MindeeApiV2.java index 5ce244bf9..8cd09bee9 100644 --- a/src/main/java/com/mindee/http/MindeeApiV2.java +++ b/src/main/java/com/mindee/http/MindeeApiV2.java @@ -1,6 +1,6 @@ package com.mindee.http; -import com.mindee.InferencePredictOptions; +import com.mindee.InferenceOptions; import com.mindee.input.LocalInputSource; import com.mindee.parsing.v2.CommonResponse; import com.mindee.parsing.v2.JobResponse; @@ -15,7 +15,7 @@ abstract public class MindeeApiV2 extends MindeeApiCommon { */ abstract public JobResponse enqueuePost( LocalInputSource inputSource, - InferencePredictOptions options + InferenceOptions options ) throws IOException; /** diff --git a/src/main/java/com/mindee/http/MindeeHttpApiV2.java b/src/main/java/com/mindee/http/MindeeHttpApiV2.java index 1cae5ba82..c1b0d0dbc 100644 --- a/src/main/java/com/mindee/http/MindeeHttpApiV2.java +++ b/src/main/java/com/mindee/http/MindeeHttpApiV2.java @@ -1,7 +1,7 @@ package com.mindee.http; import com.fasterxml.jackson.databind.ObjectMapper; -import com.mindee.InferencePredictOptions; +import com.mindee.InferenceOptions; import com.mindee.MindeeException; import com.mindee.MindeeSettingsV2; import com.mindee.input.LocalInputSource; @@ -74,7 +74,7 @@ private MindeeHttpApiV2( */ public JobResponse enqueuePost( LocalInputSource inputSource, - InferencePredictOptions options + InferenceOptions options ) { String url = this.mindeeSettings.getBaseUrl() + "/inferences/enqueue"; HttpPost post = buildHttpPost(url, inputSource, options); @@ -149,7 +149,7 @@ private MindeeHttpExceptionV2 getHttpError(ClassicHttpResponse response) { private HttpEntity buildHttpBody( LocalInputSource inputSource, - InferencePredictOptions options + InferenceOptions options ) { MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.setMode(HttpMultipartMode.EXTENDED); @@ -187,7 +187,7 @@ private HttpEntity buildHttpBody( private HttpPost buildHttpPost( String url, LocalInputSource inputSource, - InferencePredictOptions options + InferenceOptions options ) { HttpPost post; try { diff --git a/src/main/java/com/mindee/parsing/v2/Inference.java b/src/main/java/com/mindee/parsing/v2/Inference.java index 2e7d274bf..3b50dd6fe 100644 --- a/src/main/java/com/mindee/parsing/v2/Inference.java +++ b/src/main/java/com/mindee/parsing/v2/Inference.java @@ -43,18 +43,19 @@ public class Inference { @Override public String toString() { - StringJoiner sj = new StringJoiner("\n"); - sj.add("#########") + StringJoiner joiner = new StringJoiner("\n"); + joiner .add("Inference") .add("#########") - .add(":Model: " + (model != null ? model.getId() : "")) - .add(":File:") - .add(" :Name: " + (file != null ? file.getName() : "")) - .add(" :Alias: " + (file != null ? file.getAlias() : "")) + .add("Model") + .add("=====") + .add(":ID: " + (model != null ? model.getId() : "")) + .add("") + .add("File") + .add("====") + .add(file != null ? file.toString() : "") .add("") - .add("Result") - .add("======") .add(result != null ? result.toString() : ""); - return sj.toString().trim(); + return joiner.toString().trim() + "\n"; } } diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResult.java b/src/main/java/com/mindee/parsing/v2/InferenceResult.java index 4b6984b37..d9e92ba74 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceResult.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResult.java @@ -34,9 +34,13 @@ public final class InferenceResult { @Override public String toString() { StringJoiner joiner = new StringJoiner("\n"); + joiner.add("Fields") + .add("======"); joiner.add(fields.toString()); - if (options != null) { - joiner.add(options.toString()); + if (this.getOptions() != null) { + joiner.add("Options") + .add("=======") + .add(this.getOptions().toString()); } return joiner.toString(); } diff --git a/src/main/java/com/mindee/parsing/v2/InferenceResultFile.java b/src/main/java/com/mindee/parsing/v2/InferenceResultFile.java index f7f424d69..5296ae5e9 100644 --- a/src/main/java/com/mindee/parsing/v2/InferenceResultFile.java +++ b/src/main/java/com/mindee/parsing/v2/InferenceResultFile.java @@ -27,4 +27,8 @@ public class InferenceResultFile { */ @JsonProperty("alias") private String alias; + + public String toString() { + return ":Name: " + name + "\n:Alias:" + (alias != null ? " " + alias : ""); + } } diff --git a/src/main/java/com/mindee/parsing/v2/field/BaseField.java b/src/main/java/com/mindee/parsing/v2/field/BaseField.java index aef339840..f999d5632 100644 --- a/src/main/java/com/mindee/parsing/v2/field/BaseField.java +++ b/src/main/java/com/mindee/parsing/v2/field/BaseField.java @@ -1,8 +1,21 @@ package com.mindee.parsing.v2.field; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + /** * Base class for V2 fields. */ public abstract class BaseField { + /** + * Field's location. + */ + @JsonProperty("locations") + private List page; + /** + * Confidence associated with the field. + */ + @JsonProperty("confidence") + private FieldConfidence confidence; } diff --git a/src/main/java/com/mindee/parsing/v2/field/FieldConfidence.java b/src/main/java/com/mindee/parsing/v2/field/FieldConfidence.java new file mode 100644 index 000000000..8651f02f6 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/field/FieldConfidence.java @@ -0,0 +1,38 @@ +package com.mindee.parsing.v2.field; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Confidence level of a field as returned by the V2 API. + */ +public enum FieldConfidence { + Certain("Certain"), + High("High"), + Medium("Medium"), + Low("Low"); + + private final String json; + + FieldConfidence(String json) { + this.json = json; + } + + @JsonValue + public String toJson() { + return json; + } + + @JsonCreator + public static FieldConfidence fromJson(String value) { + if (value == null) { + return null; + } + for (FieldConfidence level : values()) { + if (level.json.equalsIgnoreCase(value)) { + return level; + } + } + throw new IllegalArgumentException("Unknown confidence level '" + value + "'."); + } +} diff --git a/src/main/java/com/mindee/parsing/v2/field/FieldLocation.java b/src/main/java/com/mindee/parsing/v2/field/FieldLocation.java new file mode 100644 index 000000000..dea8b6783 --- /dev/null +++ b/src/main/java/com/mindee/parsing/v2/field/FieldLocation.java @@ -0,0 +1,35 @@ +package com.mindee.parsing.v2.field; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.mindee.geometry.Polygon; +import com.mindee.geometry.PolygonDeserializer; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Location data for a field. + */ +@Getter +@EqualsAndHashCode +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class FieldLocation { + + /** + * Free polygon made up of points. + */ + @JsonProperty("polygon") + @JsonDeserialize(using = PolygonDeserializer.class) + private Polygon polygon; + + /** + * Page ID. + */ + @JsonProperty("page") + private int page; +} diff --git a/src/main/java/com/mindee/parsing/v2/field/InferenceFields.java b/src/main/java/com/mindee/parsing/v2/field/InferenceFields.java index 3f17c2600..1b5b8e374 100644 --- a/src/main/java/com/mindee/parsing/v2/field/InferenceFields.java +++ b/src/main/java/com/mindee/parsing/v2/field/InferenceFields.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.mindee.parsing.SummaryHelper; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.StringJoiner; import lombok.EqualsAndHashCode; @@ -11,9 +11,9 @@ */ @EqualsAndHashCode(callSuper = true) @JsonIgnoreProperties(ignoreUnknown = true) -public final class InferenceFields extends HashMap { - @Override - public String toString() { +public final class InferenceFields extends LinkedHashMap { + public String toString(int indent) { + String padding = String.join("", java.util.Collections.nCopies(indent, " ")); if (this.isEmpty()) { return ""; } @@ -21,7 +21,7 @@ public String toString() { this.forEach((fieldKey, fieldValue) -> { StringBuilder strBuilder = new StringBuilder(); - strBuilder.append(':').append(fieldKey).append(": "); + strBuilder.append(padding).append(":").append(fieldKey).append(": "); if (fieldValue.getListField() != null) { ListField listField = fieldValue.getListField(); @@ -32,10 +32,16 @@ public String toString() { strBuilder.append(fieldValue.getObjectField()); } else if (fieldValue.getSimpleField() != null) { strBuilder.append(fieldValue.getSimpleField().getValue() != null ? fieldValue.getSimpleField().getValue() : ""); + } joiner.add(strBuilder); }); return SummaryHelper.cleanSummary(joiner.toString()); } + + @Override + public String toString() { + return this.toString(0); + } } diff --git a/src/main/java/com/mindee/parsing/v2/field/ListField.java b/src/main/java/com/mindee/parsing/v2/field/ListField.java index 16bf49e27..1234e914e 100644 --- a/src/main/java/com/mindee/parsing/v2/field/ListField.java +++ b/src/main/java/com/mindee/parsing/v2/field/ListField.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; import java.util.List; +import java.util.StringJoiner; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -30,8 +31,17 @@ public String toString() { if (items == null || items.isEmpty()) { return "\n"; } - StringBuilder strBuilder = new StringBuilder(); - items.forEach(f -> strBuilder.append(f == null ? "" : f.toString())); - return strBuilder.toString(); + StringJoiner joiner = new StringJoiner("\n * "); + joiner.add(""); + for (DynamicField item : items) { + if (item != null) { + if (item.getType() == DynamicField.FieldType.OBJECT_FIELD) { + joiner.add(item.getObjectField().toStringFromList()); + } else { + joiner.add(item.toString()); + } + } + } + return joiner.toString(); } } 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 90c44a5de..36991c12a 100644 --- a/src/main/java/com/mindee/parsing/v2/field/ObjectField.java +++ b/src/main/java/com/mindee/parsing/v2/field/ObjectField.java @@ -25,7 +25,10 @@ public class ObjectField extends BaseField { @Override public String toString() { - return "\n" + (fields != null ? fields.toString() : ""); + return "\n" + (fields != null ? fields.toString(1) : ""); } + public String toStringFromList(){ + return fields != null ? fields.toString(2).substring(4) : ""; + } } diff --git a/src/test/java/com/mindee/MindeeClientV2IT.java b/src/test/java/com/mindee/MindeeClientV2IT.java index 427f8a6cf..a3e42c0b3 100644 --- a/src/test/java/com/mindee/MindeeClientV2IT.java +++ b/src/test/java/com/mindee/MindeeClientV2IT.java @@ -40,8 +40,8 @@ void parseFile_emptyMultiPage_mustSucceed() throws IOException, InterruptedExcep LocalInputSource source = new LocalInputSource( new File("src/test/resources/file_types/pdf/multipage_cut-2.pdf")); - InferencePredictOptions options = - InferencePredictOptions.builder(modelId).build(); + InferenceOptions options = + InferenceOptions.builder(modelId).build(); InferenceResponse response = mindeeClient.enqueueAndParse(source, options); @@ -64,8 +64,8 @@ void parseFile_filledSinglePage_mustSucceed() throws IOException, InterruptedExc LocalInputSource source = new LocalInputSource( new File("src/test/resources/products/financial_document/default_sample.jpg")); - InferencePredictOptions options = - InferencePredictOptions.builder(modelId).build(); + InferenceOptions options = + InferenceOptions.builder(modelId).build(); InferenceResponse response = mindeeClient.enqueueAndParse(source, options); @@ -98,8 +98,8 @@ void invalidModel_mustThrowError() throws IOException { LocalInputSource source = new LocalInputSource( new File("src/test/resources/file_types/pdf/multipage_cut-2.pdf")); - InferencePredictOptions options = - InferencePredictOptions.builder("INVALID MODEL ID").build(); + InferenceOptions options = + InferenceOptions.builder("INVALID MODEL ID").build(); MindeeHttpExceptionV2 ex = assertThrows( MindeeHttpExceptionV2.class, diff --git a/src/test/java/com/mindee/MindeeClientV2Test.java b/src/test/java/com/mindee/MindeeClientV2Test.java index 15eb5945b..91831b420 100644 --- a/src/test/java/com/mindee/MindeeClientV2Test.java +++ b/src/test/java/com/mindee/MindeeClientV2Test.java @@ -33,7 +33,7 @@ class Enqueue { @DisplayName("sends exactly one HTTP call and yields a non-null response") void enqueue_post_async() throws IOException { MindeeApiV2 predictable = Mockito.mock(MindeeApiV2.class); - when(predictable.enqueuePost(any(LocalInputSource.class), any(InferencePredictOptions.class))) + when(predictable.enqueuePost(any(LocalInputSource.class), any(InferenceOptions.class))) .thenReturn(new JobResponse()); MindeeClientV2 mindeeClient = makeClientWithMockedApi(predictable); @@ -42,12 +42,12 @@ void enqueue_post_async() throws IOException { new LocalInputSource(new File("src/test/resources/file_types/pdf/blank_1.pdf")); JobResponse response = mindeeClient.enqueue( input, - InferencePredictOptions.builder("dummy-model-id").build() + InferenceOptions.builder("dummy-model-id").build() ); assertNotNull(response, "enqueue() must return a response"); verify(predictable, atMostOnce()) - .enqueuePost(any(LocalInputSource.class), any(InferencePredictOptions.class)); + .enqueuePost(any(LocalInputSource.class), any(InferenceOptions.class)); } } diff --git a/src/test/java/com/mindee/v2/InferenceTest.java b/src/test/java/com/mindee/parsing/v2/InferenceTest.java similarity index 90% rename from src/test/java/com/mindee/v2/InferenceTest.java rename to src/test/java/com/mindee/parsing/v2/InferenceTest.java index dea0c9dbf..6c9df015d 100644 --- a/src/test/java/com/mindee/v2/InferenceTest.java +++ b/src/test/java/com/mindee/parsing/v2/InferenceTest.java @@ -1,18 +1,19 @@ -package com.mindee.v2; +package com.mindee.parsing.v2; import com.mindee.MindeeClientV2; import com.mindee.input.LocalResponse; -import com.mindee.parsing.v2.*; import com.mindee.parsing.v2.field.*; import com.mindee.parsing.v2.field.DynamicField.FieldType; import java.io.IOException; +import java.io.PrintWriter; 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 org.junit.jupiter.api.Assertions.*; @DisplayName("InferenceV2 – field integrity checks") @@ -23,6 +24,13 @@ private InferenceResponse loadFromResource(String resourcePath) throws IOExcepti return dummyClient.loadInference(new LocalResponse(InferenceTest.class.getClassLoader().getResourceAsStream(resourcePath))); } + private String readFileAsString(String path) + throws IOException + { + byte[] encoded = IOUtils.toByteArray(Objects.requireNonNull(InferenceTest.class.getClassLoader().getResourceAsStream(path))); + return new String(encoded); + } + @Nested @DisplayName("When the async prediction is blank") @@ -237,4 +245,21 @@ void fullInferenceResponse_mustExposeEveryProperty() throws IOException { assertNull(inf.getResult().getOptions()); } } + + @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"); + Inference inf = resp.getInference(); + try (PrintWriter out = new PrintWriter("local_test/dump.txt")){ + out.write(String.valueOf(resp.getInference())); + } + assertNotNull(inf); + assertEquals(rstRef, resp.getInference().toString()); + } + } } diff --git a/src/test/resources b/src/test/resources index e2912fbd3..f43634e5b 160000 --- a/src/test/resources +++ b/src/test/resources @@ -1 +1 @@ -Subproject commit e2912fbd362b7ccf595a5a8d6cc6a67f78901cde +Subproject commit f43634e5b7c7f773c9c3dbec461b143c21a8f6d3