From 942e33b2481c20fcd26c5c847a19434955c59fe8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 10:57:16 +0000 Subject: [PATCH 1/5] refactor: make Java client idiomatic (enums, Stream iteration, abort overloads) Breaking changes to the public interface: - RunStatus/RunOrigin/PermissionLevel/WebhookEventType enums replace stringly-typed status, origin, permission level and webhook event type fields/params. - store().iterate() and requestQueue().paginateRequests() return Stream. - RunClient.abort(Boolean) split into abort() / abort(boolean gracefully). Consistency target is the JS reference implementation only. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JvkiV9GQnM5qo4iAnX43K7 --- CHANGELOG.md | 23 +++++++ docs/README.md | 2 +- docs/actors.md | 19 ++--- docs/builds.md | 7 +- docs/examples.md | 11 ++- docs/misc.md | 12 ++-- docs/runs.md | 14 ++-- docs/storages.md | 9 +-- docs/tasks.md | 2 +- docs/webhooks.md | 11 +-- pom.xml | 2 +- .../java/com/apify/client/ActorClient.java | 6 +- src/main/java/com/apify/client/ActorRun.java | 12 ++-- .../com/apify/client/ActorStartOptions.java | 13 ++-- src/main/java/com/apify/client/Build.java | 12 ++-- .../java/com/apify/client/LastRunOptions.java | 20 +++--- .../com/apify/client/PermissionLevel.java | 26 +++++++ .../com/apify/client/RequestQueueClient.java | 16 +++-- src/main/java/com/apify/client/RunClient.java | 14 +++- .../java/com/apify/client/RunListOptions.java | 12 ++-- src/main/java/com/apify/client/RunOrigin.java | 63 +++++++++++++++++ src/main/java/com/apify/client/RunStatus.java | 69 +++++++++++++++++++ src/main/java/com/apify/client/Statuses.java | 17 ----- .../apify/client/StoreCollectionClient.java | 16 +++-- .../java/com/apify/client/TaskClient.java | 6 +- src/main/java/com/apify/client/Version.java | 2 +- src/main/java/com/apify/client/Webhook.java | 4 +- .../com/apify/client/WebhookEventType.java | 68 ++++++++++++++++++ .../com/apify/client/ReviewFixesTest.java | 16 ++--- .../apify/client/examples/IterateStore.java | 18 ++--- .../examples/RunAndLastRunStorages.java | 4 +- .../integration/ActorRunIntegrationTest.java | 12 ++-- .../RequestQueueIntegrationTest.java | 6 +- .../integration/StoreIntegrationTest.java | 18 +++-- 34 files changed, 404 insertions(+), 158 deletions(-) create mode 100644 src/main/java/com/apify/client/PermissionLevel.java create mode 100644 src/main/java/com/apify/client/RunOrigin.java create mode 100644 src/main/java/com/apify/client/RunStatus.java delete mode 100644 src/main/java/com/apify/client/Statuses.java create mode 100644 src/main/java/com/apify/client/WebhookEventType.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fca00c..9d3ec9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to the Apify Java client are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2026-07-09 + +Idiomatic-Java refactor. This release contains breaking changes to the public interface. + +### Changed + +- `ActorRun.getStatus()` and `Build.getStatus()` now return a `RunStatus` enum instead of a `String`. +- `Webhook.getEventTypes()` now returns a `List` instead of a `List`. +- `ActorClient.lastRun(...)` and `TaskClient.lastRun(...)` now take a `RunStatus` instead of a + `String`; `LastRunOptions.status(...)`/`origin(...)` now take `RunStatus`/`RunOrigin` enums. +- `RunListOptions.status(...)` now takes a `List` instead of a `List`. +- `ActorStartOptions.forcePermissionLevel(...)` now takes a `PermissionLevel` enum instead of a + `String`. +- `StoreCollectionClient.iterate(...)` and `RequestQueueClient.paginateRequests(...)` now return a + lazy `Stream` instead of an `Iterator`. +- `RunClient.abort(Boolean)` is replaced by the overloads `abort()` (server default) and + `abort(boolean gracefully)`. + +### Added + +- `RunStatus`, `RunOrigin`, `PermissionLevel` and `WebhookEventType` enums (each parsing unrecognised + server values to an `UNKNOWN` constant where applicable), with `RunStatus.isTerminal()`. + ## [0.1.1] - 2026-07-07 ### Changed diff --git a/docs/README.md b/docs/README.md index d390635..08be9bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,7 +29,7 @@ empty `Optional` rather than an exception. API failures are thrown as `ApifyApiE Snippets in these docs assume the client types are imported from `com.apify.client` (e.g. `import com.apify.client.*;`) plus standard-library types (`java.util.List`, `java.util.Map`, -`java.util.Optional`, `java.util.Iterator`, `java.time.Duration`, `java.io.InputStream`). +`java.util.Optional`, `java.util.stream.Stream`, `java.time.Duration`, `java.io.InputStream`). Raw-JSON return values use Jackson's `com.fasterxml.jackson.databind.JsonNode`. Jackson is a transitive dependency of this client, so it is already on your classpath. diff --git a/docs/actors.md b/docs/actors.md index c0586a2..59a3c12 100644 --- a/docs/actors.md +++ b/docs/actors.md @@ -50,24 +50,27 @@ Actor created = client.actors().create(Map.of( | `validateInput(Object input)` / `validateInput(Object input, ValidateInputOptions)` | Validate an input against the Actor's input schema. Returns `boolean`. `ValidateInputOptions` fields (optional): `build`, `contentType`. | | `build(String versionNumber, ActorBuildOptions)` | Build a version. Returns `Build`. | | `defaultBuild(Long waitForFinish)` | Resolve the default build. Returns `BuildClient`. | -| `lastRun(String status)` / `lastRun(LastRunOptions)` | A `RunClient` for the last run. | +| `lastRun(RunStatus status)` / `lastRun(LastRunOptions)` | A `RunClient` for the last run. | | `builds()` / `runs()` / `versions()` | Nested collection clients. | | `webhooks()` | Read-only nested webhook collection (`NestedWebhookCollectionClient`, list only). | | `version(String)` | An `ActorVersionClient`. | `ActorStartOptions` fields (all optional): `build`, `memoryMbytes`, `timeoutSecs`, `waitForFinish`, `maxItems`, `maxTotalChargeUsd`, `contentType`, `restartOnError`, `forcePermissionLevel` -(`LIMITED_PERMISSIONS`/`FULL_PERMISSIONS`), and `webhooks(List)` — ad-hoc webhook definitions -(each a JSON-serializable `Map`, as in [Webhooks](webhooks.md)) that the client base64-encodes on the -wire. +(a `PermissionLevel` enum: `LIMITED_PERMISSIONS`/`FULL_PERMISSIONS`), and `webhooks(List)` — +ad-hoc webhook definitions (each a JSON-serializable `Map`, as in [Webhooks](webhooks.md)) that the +client base64-encodes on the wire. -`lastRun(String status)` filters only by status; `lastRun(LastRunOptions)` also accepts an origin -filter. `LastRunOptions` has fluent setters `status(String)` (e.g. `SUCCEEDED`, `RUNNING`) and -`origin(String)` (e.g. `API`, `WEB`, `SCHEDULER`); leave a setter uncalled to omit that filter. +`lastRun(RunStatus status)` filters only by status; `lastRun(LastRunOptions)` also accepts an origin +filter. `LastRunOptions` has fluent setters `status(RunStatus)` (e.g. `RunStatus.SUCCEEDED`, +`RunStatus.RUNNING`) and `origin(RunOrigin)` (e.g. `RunOrigin.API`, `RunOrigin.WEB`, +`RunOrigin.SCHEDULER`); leave a setter uncalled to omit that filter. ```java Optional last = - client.actor("apify/hello-world").lastRun(new LastRunOptions().status("SUCCEEDED").origin("API")).get(); + client.actor("apify/hello-world") + .lastRun(new LastRunOptions().status(RunStatus.SUCCEEDED).origin(RunOrigin.API)) + .get(); ``` ```java diff --git a/docs/builds.md b/docs/builds.md index f6fe317..6cf0f2f 100644 --- a/docs/builds.md +++ b/docs/builds.md @@ -21,9 +21,10 @@ builds) and a single build with `client.build(id)`. | `getOpenApiDefinition()` | The build's OpenAPI definition. Returns `Optional`. | | `log()` | A `LogClient` for the build's log. | -`Build` fields: `getId()`, `getActId()`, `getStatus()`, `getStartedAt()`, `getFinishedAt()`, -`getBuildNumber()`, plus `isTerminal()` and `getExtra()`. The status is one of `READY`, `RUNNING`, -`SUCCEEDED`, `FAILED`, `TIMING-OUT`, `TIMED-OUT`, `ABORTING`, `ABORTED`. +`Build` fields: `getId()`, `getActId()`, `getStatus()` (a `RunStatus` enum), `getStartedAt()`, +`getFinishedAt()`, `getBuildNumber()`, plus `isTerminal()` and `getExtra()`. `RunStatus` is one of +`READY`, `RUNNING`, `SUCCEEDED`, `FAILED`, `TIMING_OUT`, `TIMED_OUT`, `ABORTING`, `ABORTED`, or +`UNKNOWN`. ```java Build build = client.actor("me/my-actor").build("0.0", new ActorBuildOptions().tag("latest")); diff --git a/docs/examples.md b/docs/examples.md index 23ab709..0f288e4 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -87,7 +87,7 @@ try { ```java client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L); -Optional last = client.actor("apify/hello-world").lastRun("SUCCEEDED").get(); +Optional last = client.actor("apify/hello-world").lastRun(RunStatus.SUCCEEDED).get(); if (last.isPresent()) { ActorRun run = last.get(); client.dataset(run.getDefaultDatasetId()).listItems(new DatasetListItemsOptions()); @@ -98,12 +98,9 @@ if (last.isPresent()) { ## Lazy iteration of Store Actors ```java -Iterator it = client.store().iterate(new StoreListOptions().limit(10L)); -int shown = 0; -while (shown < 5 && it.hasNext()) { - System.out.println(it.next().getName()); - shown++; -} +client.store().iterate(new StoreListOptions().limit(10L)) + .limit(5) + .forEach(item -> System.out.println(item.getName())); ``` ## Run an Actor with log redirection diff --git a/docs/misc.md b/docs/misc.md index 2c19e7d..1819cf0 100644 --- a/docs/misc.md +++ b/docs/misc.md @@ -7,20 +7,16 @@ Browse public Actors in the Apify Store. | Method | Description | |---|---| | `list(StoreListOptions)` | A page of Store Actors. Returns `PaginationList`. | -| `iterate(StoreListOptions)` | A lazy `Iterator` fetching pages on demand. | +| `iterate(StoreListOptions)` | A lazy `Stream` fetching pages on demand. | `StoreListOptions` fields: `offset`, `limit`, `search`, `sortBy`, `category`, `username`, `pricingModel` (`FREE`, `FLAT_PRICE_PER_MONTH`, `PRICE_PER_DATASET_ITEM`, `PAY_PER_EVENT`), `includeUnrunnableActors`, `allowsAgenticUsers`, `responseFormat` (`full`, `agent`). ```java -Iterator it = client.store().iterate(new StoreListOptions().search("crawler").limit(20L)); -int shown = 0; -while (shown < 5 && it.hasNext()) { - ActorStoreListItem item = it.next(); - System.out.println(item.getUsername() + "/" + item.getName()); - shown++; -} +client.store().iterate(new StoreListOptions().search("crawler").limit(20L)) + .limit(5) + .forEach(item -> System.out.println(item.getUsername() + "/" + item.getName())); ``` `ActorStoreListItem` fields: `getId()`, `getName()`, `getUsername()`, `getTitle()`. diff --git a/docs/runs.md b/docs/runs.md index fd7ab21..adb38b0 100644 --- a/docs/runs.md +++ b/docs/runs.md @@ -9,13 +9,14 @@ Access the run collection with `client.runs()` (or `client.actor(id).runs()` / |---|---| | `list(ListOptions, RunListOptions)` | List runs. Returns `PaginationList`. | -`RunListOptions` adds `status(List)` (e.g. `SUCCEEDED`, `RUNNING`; sent comma-separated) and, -for Actor/task-scoped collections, `startedAfter(String)` / `startedBefore(String)` (ISO-8601). +`RunListOptions` adds `status(List)` (e.g. `RunStatus.SUCCEEDED`, `RunStatus.RUNNING`; sent +comma-separated) and, for Actor/task-scoped collections, `startedAfter(String)` / +`startedBefore(String)` (ISO-8601). ```java PaginationList runs = client.runs().list( new ListOptions().limit(10L), - new RunListOptions().status(List.of("SUCCEEDED"))); + new RunListOptions().status(List.of(RunStatus.SUCCEEDED))); ``` ## `RunClient` @@ -26,7 +27,7 @@ PaginationList runs = client.runs().list( | `getWithWait(Long waitForFinishSecs)` | Fetch, optionally waiting server-side for the run to finish (clamped to the request timeout; the API caps server-side waiting at 60s). Returns `Optional`. | | `update(Object)` | Update the run. Returns `ActorRun`. | | `delete()` | Delete the run. | -| `abort(Boolean gracefully)` | Abort the run (`null` = server default). Returns `ActorRun`. | +| `abort()` / `abort(boolean gracefully)` | Abort the run (no-arg = server default). Returns `ActorRun`. | | `metamorph(String targetActorId, Object input, MetamorphOptions)` | Metamorph into another Actor. Returns `ActorRun`. | | `reboot()` | Reboot the run. Returns `ActorRun`. | | `resurrect(RunResurrectOptions)` | Resurrect a finished run. Returns `ActorRun`. | @@ -42,9 +43,12 @@ use `log()` for the full log text or for non-raw/download options. To set the current run's status message from inside an Actor, use the top-level `client.setStatusMessage(...)` (see [the docs index](README.md#setting-single-resource-status)). -`ActorRun` fields include `getId()`, `getActId()`, `getUserId()`, `getStatus()`, +`ActorRun` fields include `getId()`, `getActId()`, `getUserId()`, `getStatus()` (a `RunStatus` enum), `getStatusMessage()`, `getStartedAt()`, `getFinishedAt()`, `getBuildId()`, `getDefaultDatasetId()`, `getDefaultKeyValueStoreId()`, `getDefaultRequestQueueId()`, `getContainerUrl()`, plus `isTerminal()`. +`RunStatus` is the run/build lifecycle enum (`READY`, `RUNNING`, `SUCCEEDED`, `FAILED`, `TIMING_OUT`, +`TIMED_OUT`, `ABORTING`, `ABORTED`, and `UNKNOWN` for a value this client version does not recognise); +`RunStatus.isTerminal()` reports whether a status is final. `RunChargeOptions` (constructed with the required event name) uses plain values: `count(Long)` and `idempotencyKey(String)` — the latter is auto-generated when unset so a transport-retried charge is diff --git a/docs/storages.md b/docs/storages.md index ab35bfc..4e67963 100644 --- a/docs/storages.md +++ b/docs/storages.md @@ -65,7 +65,7 @@ unsigned. > **Public-URL limitation (matches the JavaScript reference client).** The public-URL builders > (`createItemsPublicUrl`, and the key-value-store `getRecordPublicUrl` / `createKeysPublicUrl`) > build the URL from the client's own resource path plus the signature. When called on a *last-run -> nested* client (e.g. `client.actor(id).lastRun("SUCCEEDED").dataset().createItemsPublicUrl(...)`) +> nested* client (e.g. `client.actor(id).lastRun(RunStatus.SUCCEEDED).dataset().createItemsPublicUrl(...)`) > the returned URL keeps the unpinned `.../runs/last/dataset` path and does not carry the `status` > filter, so it may resolve a different run when opened. Build public URLs from a concrete > `client.dataset(id)` / `client.keyValueStore(id)` client when you need a stable shareable URL. @@ -138,16 +138,13 @@ expiry-aware storage-content signature — hence only `createKeysPublicUrl` take | `prolongRequestLock(String id, long lockSecs, boolean forefront)` | Extend a lock. Returns `JsonNode`. | | `deleteRequestLock(String id, boolean forefront)` | Release a lock. No return value. | | `unlockRequests()` | Release all the client's locks. Returns `JsonNode`. | -| `paginateRequests(Long pageLimit)` | A lazy `Iterator` over all requests. | +| `paginateRequests(Long pageLimit)` | A lazy `Stream` over all requests. | ```java RequestQueue rq = client.requestQueues().getOrCreate("my-queue"); RequestQueueClient queue = client.requestQueue(rq.getId()); queue.addRequest(new RequestQueueRequest("https://example.com", "example"), false); -Iterator it = queue.paginateRequests(100L); -while (it.hasNext()) { - System.out.println(it.next().getUrl()); -} +queue.paginateRequests(100L).forEach(request -> System.out.println(request.getUrl())); ``` `ListRequestsOptions` fields: `limit`, `exclusiveStartId`, `cursor` (mutually exclusive with diff --git a/docs/tasks.md b/docs/tasks.md index 2db0d62..537589a 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -27,7 +27,7 @@ Task task = client.tasks().create(Map.of( | `call(Object input, TaskStartOptions, Long waitSecs)` | Start and poll until finished. | | `getInput()` | The stored input. Returns `Optional`. | | `updateInput(Object)` | Replace the stored input. Returns `JsonNode`. | -| `lastRun(String status)` / `lastRun(LastRunOptions)` | A `RunClient` for the last run (see [`LastRunOptions`](actors.md#actorclient)). | +| `lastRun(RunStatus status)` / `lastRun(LastRunOptions)` | A `RunClient` for the last run (see [`LastRunOptions`](actors.md#actorclient)). | | `runs()` | Nested run collection client. | | `webhooks()` | Read-only nested webhook collection (`NestedWebhookCollectionClient`, list only). | diff --git a/docs/webhooks.md b/docs/webhooks.md index 59a8533..4eba06a 100644 --- a/docs/webhooks.md +++ b/docs/webhooks.md @@ -23,10 +23,12 @@ exposes `list(ListOptions)` only — it has no `create(...)`. To create a webhoo Actor or task, use `client.webhooks().create(...)` and set the Actor/task in the webhook's `condition`. -A webhook definition supplies `eventTypes` (a list of event-type strings such as +A webhook definition supplies `eventTypes` (the events that trigger it, such as `ACTOR.RUN.SUCCEEDED`, `ACTOR.RUN.FAILED`, `ACTOR.RUN.ABORTED`, `ACTOR.RUN.TIMED_OUT`, -`ACTOR.RUN.CREATED`, `ACTOR.BUILD.SUCCEEDED`, …), a `condition` and a `requestUrl`. The definition is -a plain JSON-serializable value (e.g. a `Map`); this client does not wrap it in a typed enum: +`ACTOR.RUN.CREATED`, `ACTOR.BUILD.SUCCEEDED`, …), a `condition` and a `requestUrl`. The definition you +pass to `create(...)` is a plain JSON-serializable value (e.g. a `Map`), so its event types are wire +strings; when reading a webhook back, `Webhook.getEventTypes()` returns them as a typed +`WebhookEventType` enum (with `UNKNOWN` for any value this client version does not recognise): ```java Webhook webhook = client.webhooks().create(Map.of( @@ -49,7 +51,8 @@ WebhookDispatch dispatch = client.webhook("WEBHOOK_ID").test(); System.out.println(dispatch.getId()); ``` -`Webhook` fields: `getId()`, `getUserId()`, `getRequestUrl()`, `getEventTypes()`. +`Webhook` fields: `getId()`, `getUserId()`, `getRequestUrl()`, `getEventTypes()` (a +`List`). ## `WebhookDispatchCollectionClient` and `WebhookDispatchClient` diff --git a/pom.xml b/pom.xml index a1d1372..bae03b6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.apify apify-client - 0.1.1 + 0.2.0 jar Apify Java Client diff --git a/src/main/java/com/apify/client/ActorClient.java b/src/main/java/com/apify/client/ActorClient.java index ad05ddd..ffff68c 100644 --- a/src/main/java/com/apify/client/ActorClient.java +++ b/src/main/java/com/apify/client/ActorClient.java @@ -112,10 +112,10 @@ public BuildClient defaultBuild(Long waitForFinish) { } /** - * Returns a client for the last run of this Actor, optionally filtered by status (e.g. {@code - * "SUCCEEDED"}). Pass {@code null} or empty for no filter. + * Returns a client for the last run of this Actor, optionally filtered by status (e.g. {@link + * RunStatus#SUCCEEDED}). Pass {@code null} for no filter. */ - public RunClient lastRun(String status) { + public RunClient lastRun(RunStatus status) { return lastRun(new LastRunOptions().status(status)); } diff --git a/src/main/java/com/apify/client/ActorRun.java b/src/main/java/com/apify/client/ActorRun.java index 31e635b..4998f31 100644 --- a/src/main/java/com/apify/client/ActorRun.java +++ b/src/main/java/com/apify/client/ActorRun.java @@ -8,7 +8,7 @@ public final class ActorRun extends ApifyResource { private String actId; private String actorTaskId; private String userId; - private String status; + private RunStatus status; private String statusMessage; private Instant startedAt; private Instant finishedAt; @@ -38,12 +38,8 @@ public String getUserId() { return userId; } - /** - * The current run status. One of the eight {@code ActorJobStatus} values: {@code READY}, {@code - * RUNNING}, {@code SUCCEEDED}, {@code FAILED}, {@code TIMING-OUT}, {@code TIMED-OUT}, {@code - * ABORTING}, {@code ABORTED}. - */ - public String getStatus() { + /** The current run status, or {@code null} if the API did not report one. */ + public RunStatus getStatus() { return status; } @@ -89,6 +85,6 @@ public String getContainerUrl() { /** Whether the run has reached a terminal (finished) status. */ public boolean isTerminal() { - return Statuses.isTerminal(status); + return status != null && status.isTerminal(); } } diff --git a/src/main/java/com/apify/client/ActorStartOptions.java b/src/main/java/com/apify/client/ActorStartOptions.java index 3d5c111..edcbbfd 100644 --- a/src/main/java/com/apify/client/ActorStartOptions.java +++ b/src/main/java/com/apify/client/ActorStartOptions.java @@ -16,7 +16,7 @@ public final class ActorStartOptions { private Double maxTotalChargeUsd; private String contentType; private Boolean restartOnError; - private String forcePermissionLevel; + private PermissionLevel forcePermissionLevel; private List webhooks; /** The tag or number of the build to run (e.g. {@code "latest"}, {@code "0.1.2"}). */ @@ -67,11 +67,8 @@ public ActorStartOptions restartOnError(Boolean restartOnError) { return this; } - /** - * Override the Actor's permission level for this run ({@code LIMITED_PERMISSIONS}/{@code - * FULL_PERMISSIONS}). - */ - public ActorStartOptions forcePermissionLevel(String forcePermissionLevel) { + /** Override the Actor's permission level for this run. */ + public ActorStartOptions forcePermissionLevel(PermissionLevel forcePermissionLevel) { this.forcePermissionLevel = forcePermissionLevel; return this; } @@ -99,7 +96,9 @@ void apply(QueryParams q) { .addLong("maxItems", maxItems) .addDouble("maxTotalChargeUsd", maxTotalChargeUsd) .addBool("restartOnError", restartOnError) - .addString("forcePermissionLevel", forcePermissionLevel); + .addString( + "forcePermissionLevel", + forcePermissionLevel == null ? null : forcePermissionLevel.getWireValue()); q.addString("webhooks", encodeWebhooks(webhooks)); } diff --git a/src/main/java/com/apify/client/Build.java b/src/main/java/com/apify/client/Build.java index ebbc867..72d8750 100644 --- a/src/main/java/com/apify/client/Build.java +++ b/src/main/java/com/apify/client/Build.java @@ -6,7 +6,7 @@ public final class Build extends ApifyResource { private String id; private String actId; - private String status; + private RunStatus status; private Instant startedAt; private Instant finishedAt; private String buildNumber; @@ -21,12 +21,8 @@ public String getActId() { return actId; } - /** - * The current build status. One of the eight {@code ActorJobStatus} values: {@code READY}, {@code - * RUNNING}, {@code SUCCEEDED}, {@code FAILED}, {@code TIMING-OUT}, {@code TIMED-OUT}, {@code - * ABORTING}, {@code ABORTED}. - */ - public String getStatus() { + /** The current build status, or {@code null} if the API did not report one. */ + public RunStatus getStatus() { return status; } @@ -47,6 +43,6 @@ public String getBuildNumber() { /** Whether the build has reached a terminal (finished) status. */ public boolean isTerminal() { - return Statuses.isTerminal(status); + return status != null && status.isTerminal(); } } diff --git a/src/main/java/com/apify/client/LastRunOptions.java b/src/main/java/com/apify/client/LastRunOptions.java index 0a582c1..5481d5e 100644 --- a/src/main/java/com/apify/client/LastRunOptions.java +++ b/src/main/java/com/apify/client/LastRunOptions.java @@ -4,34 +4,32 @@ * Filters which "last" run the {@link ActorClient#lastRun}/{@link TaskClient#lastRun} accessors * resolve to. Leave a field unset to leave that filter unset. * - *

{@code origin} is now a spec-declared query parameter on the {@code runs/last} endpoints + *

{@code origin} is a spec-declared query parameter on the {@code runs/last} endpoints * (alongside {@code status}), matching the reference client's {@code lastRun({status, origin})}. * The spec also declares {@code waitForFinish} on those endpoints, but the reference client does * not expose it on {@code lastRun}, so neither do we. */ public final class LastRunOptions { - private String status; - private String origin; + private RunStatus status; + private RunOrigin origin; - /** Filter by run status (e.g. {@code "SUCCEEDED"}, {@code "FAILED"}, {@code "RUNNING"}). */ - public LastRunOptions status(String status) { + /** Filter by run status (e.g. {@link RunStatus#SUCCEEDED}). */ + public LastRunOptions status(RunStatus status) { this.status = status; return this; } - /** - * Filter by how the run was started (e.g. {@code "DEVELOPMENT"}, {@code "WEB"}, {@code "API"}). - */ - public LastRunOptions origin(String origin) { + /** Filter by how the run was started (e.g. {@link RunOrigin#API}). */ + public LastRunOptions origin(RunOrigin origin) { this.origin = origin; return this; } String statusValue() { - return status; + return status == null ? null : status.getWireValue(); } String originValue() { - return origin; + return origin == null ? null : origin.getWireValue(); } } diff --git a/src/main/java/com/apify/client/PermissionLevel.java b/src/main/java/com/apify/client/PermissionLevel.java new file mode 100644 index 0000000..2c11bbe --- /dev/null +++ b/src/main/java/com/apify/client/PermissionLevel.java @@ -0,0 +1,26 @@ +package com.apify.client; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The permission level an Actor run executes with, used to override the Actor's default via {@link + * ActorStartOptions#forcePermissionLevel(PermissionLevel)}. + */ +public enum PermissionLevel { + /** The run is restricted to a limited set of permissions. */ + LIMITED_PERMISSIONS("LIMITED_PERMISSIONS"), + /** The run has the Actor's full set of permissions. */ + FULL_PERMISSIONS("FULL_PERMISSIONS"); + + private final String wireValue; + + PermissionLevel(String wireValue) { + this.wireValue = wireValue; + } + + /** The value sent in the {@code forcePermissionLevel} query parameter. */ + @JsonValue + public String getWireValue() { + return wireValue; + } +} diff --git a/src/main/java/com/apify/client/RequestQueueClient.java b/src/main/java/com/apify/client/RequestQueueClient.java index c2d4f83..e7f6f10 100644 --- a/src/main/java/com/apify/client/RequestQueueClient.java +++ b/src/main/java/com/apify/client/RequestQueueClient.java @@ -9,11 +9,15 @@ import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; /** A client for a specific request queue (and run-nested variants). */ public final class RequestQueueClient { @@ -411,11 +415,15 @@ public JsonNode unlockRequests() { } /** - * Returns a lazy iterator over all requests in the queue, fetching pages of up to {@code - * pageLimit} requests at a time ({@code null} for the server default). + * Returns a lazy {@link Stream} over all requests in the queue, fetching pages of up to {@code + * pageLimit} requests at a time ({@code null} for the server default) as the stream is consumed. + * The stream is sequential and need not be closed. */ - public Iterator paginateRequests(Long pageLimit) { - return new RequestsIterator(pageLimit); + public Stream paginateRequests(Long pageLimit) { + Spliterator spliterator = + Spliterators.spliteratorUnknownSize( + new RequestsIterator(pageLimit), Spliterator.ORDERED | Spliterator.NONNULL); + return StreamSupport.stream(spliterator, false); } /** Shape of a paginated requests listing. */ diff --git a/src/main/java/com/apify/client/RunClient.java b/src/main/java/com/apify/client/RunClient.java index 374e77f..b8ea091 100644 --- a/src/main/java/com/apify/client/RunClient.java +++ b/src/main/java/com/apify/client/RunClient.java @@ -68,12 +68,20 @@ public void delete() { ctx.deleteResource(""); } + /** Aborts the run immediately, applying the server default (an immediate abort). */ + public ActorRun abort() { + return abortInternal(null); + } + /** * Aborts the run. If {@code gracefully} is {@code true}, the run is signalled so it can finish - * the current request before terminating; if {@code false} it is aborted immediately. Pass {@code - * null} to omit the parameter and let the server apply its default (immediate abort). + * the current request before terminating; if {@code false} it is aborted immediately. */ - public ActorRun abort(Boolean gracefully) { + public ActorRun abort(boolean gracefully) { + return abortInternal(gracefully); + } + + private ActorRun abortInternal(Boolean gracefully) { QueryParams params = new QueryParams(); params.addBool("gracefully", gracefully); return ctx.postWithBody("abort", params, null, "", ActorRun.class); diff --git a/src/main/java/com/apify/client/RunListOptions.java b/src/main/java/com/apify/client/RunListOptions.java index 3f21d70..439d231 100644 --- a/src/main/java/com/apify/client/RunListOptions.java +++ b/src/main/java/com/apify/client/RunListOptions.java @@ -8,15 +8,15 @@ * task-scoped run collections. */ public final class RunListOptions { - private List status; + private List status; private String startedAfter; private String startedBefore; /** - * Filter by one or more run statuses (e.g. {@code "SUCCEEDED"}, {@code "RUNNING"}). Sent as a - * comma-separated list, as the API accepts. + * Filter by one or more run statuses (e.g. {@link RunStatus#SUCCEEDED}, {@link + * RunStatus#RUNNING}). Sent as a comma-separated list, as the API accepts. */ - public RunListOptions status(List status) { + public RunListOptions status(List status) { this.status = status == null ? null : List.copyOf(status); return this; } @@ -34,7 +34,9 @@ public RunListOptions startedBefore(String startedBefore) { } void apply(QueryParams q) { - q.addCsv("status", status) + List statusValues = + status == null ? null : status.stream().map(RunStatus::getWireValue).toList(); + q.addCsv("status", statusValues) .addString("startedAfter", startedAfter) .addString("startedBefore", startedBefore); } diff --git a/src/main/java/com/apify/client/RunOrigin.java b/src/main/java/com/apify/client/RunOrigin.java new file mode 100644 index 0000000..2b7cb19 --- /dev/null +++ b/src/main/java/com/apify/client/RunOrigin.java @@ -0,0 +1,63 @@ +package com.apify.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * How an Actor run was started (the API's {@code RunOrigin} / {@code meta.origin}). + * + *

{@link #UNKNOWN} is returned for any origin value the API introduces that this client version + * does not yet recognise, so deserialization never fails on a newer server. + */ +public enum RunOrigin { + /** Started from the Actor's development console. */ + DEVELOPMENT("DEVELOPMENT"), + /** Started manually from the Apify Console web UI. */ + WEB("WEB"), + /** Started programmatically through the API. */ + API("API"), + /** Started by a schedule. */ + SCHEDULER("SCHEDULER"), + /** Started as part of a test. */ + TEST("TEST"), + /** Started by a webhook. */ + WEBHOOK("WEBHOOK"), + /** Started by another Actor. */ + ACTOR("ACTOR"), + /** Started by a continuous-integration pipeline. */ + CI("CI"), + /** Started from the Apify command-line interface. */ + CLI("CLI"), + /** Started from an Actor Standby request. */ + STANDBY("STANDBY"), + /** Started through the Model Context Protocol integration. */ + MCP("MCP"), + /** An origin value the API returned that this client version does not recognise. */ + UNKNOWN(null); + + private final String wireValue; + + RunOrigin(String wireValue) { + this.wireValue = wireValue; + } + + /** The value used on the wire; {@code null} for {@link #UNKNOWN}. */ + @JsonValue + public String getWireValue() { + return wireValue; + } + + /** Maps a wire value to its constant, or {@link #UNKNOWN} for an unrecognised non-null value. */ + @JsonCreator + public static RunOrigin fromWire(String value) { + if (value == null) { + return null; + } + for (RunOrigin origin : values()) { + if (value.equals(origin.wireValue)) { + return origin; + } + } + return UNKNOWN; + } +} diff --git a/src/main/java/com/apify/client/RunStatus.java b/src/main/java/com/apify/client/RunStatus.java new file mode 100644 index 0000000..7aa16c7 --- /dev/null +++ b/src/main/java/com/apify/client/RunStatus.java @@ -0,0 +1,69 @@ +package com.apify.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Set; + +/** + * The lifecycle status of an Actor run or build (the API's {@code ActorJobStatus}). + * + *

Some statuses are terminal — once reached, the run/build is finished and its status + * will not change. Use {@link #isTerminal()} to test for this rather than comparing strings. + * + *

{@link #UNKNOWN} is returned for any status value the API introduces that this client version + * does not yet recognise, so deserialization never fails on a newer server. + */ +public enum RunStatus { + /** The run/build has been created but has not started yet. */ + READY("READY"), + /** The run/build is currently executing. */ + RUNNING("RUNNING"), + /** The run/build finished successfully (terminal). */ + SUCCEEDED("SUCCEEDED"), + /** The run/build failed (terminal). */ + FAILED("FAILED"), + /** The run/build is being timed out. */ + TIMING_OUT("TIMING-OUT"), + /** The run/build was timed out (terminal). */ + TIMED_OUT("TIMED-OUT"), + /** The run/build is being aborted. */ + ABORTING("ABORTING"), + /** The run/build was aborted (terminal). */ + ABORTED("ABORTED"), + /** A status value the API returned that this client version does not recognise. */ + UNKNOWN(null); + + /** Statuses in which a run/build is finished and will not change. */ + private static final Set TERMINAL = Set.of(SUCCEEDED, FAILED, ABORTED, TIMED_OUT); + + private final String wireValue; + + RunStatus(String wireValue) { + this.wireValue = wireValue; + } + + /** The value used on the wire (e.g. {@code "TIMED-OUT"}); {@code null} for {@link #UNKNOWN}. */ + @JsonValue + public String getWireValue() { + return wireValue; + } + + /** Whether this is a terminal (finished) status. {@link #UNKNOWN} is never terminal. */ + public boolean isTerminal() { + return TERMINAL.contains(this); + } + + /** Maps a wire value to its constant, or {@link #UNKNOWN} for an unrecognised non-null value. */ + @JsonCreator + public static RunStatus fromWire(String value) { + if (value == null) { + return null; + } + for (RunStatus status : values()) { + if (value.equals(status.wireValue)) { + return status; + } + } + return UNKNOWN; + } +} diff --git a/src/main/java/com/apify/client/Statuses.java b/src/main/java/com/apify/client/Statuses.java deleted file mode 100644 index 71f74f3..0000000 --- a/src/main/java/com/apify/client/Statuses.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.apify.client; - -import java.util.Set; - -/** Run/build status helpers. Internal to the client. */ -final class Statuses { - - /** Terminal run/build statuses: a resource in any of these is finished and will not change. */ - private static final Set TERMINAL = Set.of("SUCCEEDED", "FAILED", "ABORTED", "TIMED-OUT"); - - private Statuses() {} - - /** Reports whether the status is a terminal (finished) run/build status. */ - static boolean isTerminal(String status) { - return status != null && TERMINAL.contains(status); - } -} diff --git a/src/main/java/com/apify/client/StoreCollectionClient.java b/src/main/java/com/apify/client/StoreCollectionClient.java index 2d43ca7..77519e1 100644 --- a/src/main/java/com/apify/client/StoreCollectionClient.java +++ b/src/main/java/com/apify/client/StoreCollectionClient.java @@ -3,6 +3,10 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; /** A client for browsing the Apify Store ({@code GET /v2/store}). */ public final class StoreCollectionClient { @@ -20,11 +24,15 @@ public PaginationList list(StoreListOptions options) { } /** - * Returns a lazy iterator over all Store Actors matching the options, fetching pages on demand. - * The options' {@code limit} (if set) is used as the per-page size. + * Returns a lazy {@link Stream} over all Store Actors matching the options, fetching pages on + * demand as the stream is consumed. The options' {@code limit} (if set) is used as the per-page + * size. The stream is sequential and need not be closed. */ - public Iterator iterate(StoreListOptions options) { - return new StoreIterator(options); + public Stream iterate(StoreListOptions options) { + Spliterator spliterator = + Spliterators.spliteratorUnknownSize( + new StoreIterator(options), Spliterator.ORDERED | Spliterator.NONNULL); + return StreamSupport.stream(spliterator, false); } /** Lazily iterates over Apify Store Actors, fetching one page at a time. */ diff --git a/src/main/java/com/apify/client/TaskClient.java b/src/main/java/com/apify/client/TaskClient.java index 5849be0..8b1f133 100644 --- a/src/main/java/com/apify/client/TaskClient.java +++ b/src/main/java/com/apify/client/TaskClient.java @@ -81,10 +81,10 @@ public JsonNode updateInput(Object input) { } /** - * Returns a client for the last run of this task, optionally filtered by status (e.g. {@code - * "SUCCEEDED"}). Pass {@code null} or empty for no filter. + * Returns a client for the last run of this task, optionally filtered by status (e.g. {@link + * RunStatus#SUCCEEDED}). Pass {@code null} for no filter. */ - public RunClient lastRun(String status) { + public RunClient lastRun(RunStatus status) { return lastRun(new LastRunOptions().status(status)); } diff --git a/src/main/java/com/apify/client/Version.java b/src/main/java/com/apify/client/Version.java index c15e29d..75f6a70 100644 --- a/src/main/java/com/apify/client/Version.java +++ b/src/main/java/com/apify/client/Version.java @@ -13,7 +13,7 @@ public final class Version { * The semantic version of this client library (see SemVer). * Changes to the public interface other than additive ones are considered breaking changes. */ - public static final String CLIENT_VERSION = "0.1.1"; + public static final String CLIENT_VERSION = "0.2.0"; /** * The version of the Apify OpenAPI specification this client was generated and verified against. diff --git a/src/main/java/com/apify/client/Webhook.java b/src/main/java/com/apify/client/Webhook.java index 0222c3e..de60499 100644 --- a/src/main/java/com/apify/client/Webhook.java +++ b/src/main/java/com/apify/client/Webhook.java @@ -8,7 +8,7 @@ public final class Webhook extends ApifyResource { private String id; private String userId; private String requestUrl; - private List eventTypes = List.of(); + private List eventTypes = List.of(); /** The unique webhook ID. */ public String getId() { @@ -26,7 +26,7 @@ public String getRequestUrl() { } /** The events that trigger the webhook. */ - public List getEventTypes() { + public List getEventTypes() { return Collections.unmodifiableList(eventTypes); } } diff --git a/src/main/java/com/apify/client/WebhookEventType.java b/src/main/java/com/apify/client/WebhookEventType.java new file mode 100644 index 0000000..e3f9c13 --- /dev/null +++ b/src/main/java/com/apify/client/WebhookEventType.java @@ -0,0 +1,68 @@ +package com.apify.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * An event that can trigger a webhook (the API's {@code WebhookEventType}). + * + *

{@link #UNKNOWN} is returned for any event type the API introduces that this client version + * does not yet recognise, so deserialization never fails on a newer server. + */ +public enum WebhookEventType { + /** An Actor run was created. */ + ACTOR_RUN_CREATED("ACTOR.RUN.CREATED"), + /** An Actor run succeeded. */ + ACTOR_RUN_SUCCEEDED("ACTOR.RUN.SUCCEEDED"), + /** An Actor run failed. */ + ACTOR_RUN_FAILED("ACTOR.RUN.FAILED"), + /** An Actor run was aborted. */ + ACTOR_RUN_ABORTED("ACTOR.RUN.ABORTED"), + /** An Actor run timed out. */ + ACTOR_RUN_TIMED_OUT("ACTOR.RUN.TIMED_OUT"), + /** An Actor run was resurrected. */ + ACTOR_RUN_RESURRECTED("ACTOR.RUN.RESURRECTED"), + /** An Actor build was created. */ + ACTOR_BUILD_CREATED("ACTOR.BUILD.CREATED"), + /** An Actor build succeeded. */ + ACTOR_BUILD_SUCCEEDED("ACTOR.BUILD.SUCCEEDED"), + /** An Actor build failed. */ + ACTOR_BUILD_FAILED("ACTOR.BUILD.FAILED"), + /** An Actor build was aborted. */ + ACTOR_BUILD_ABORTED("ACTOR.BUILD.ABORTED"), + /** An Actor build timed out. */ + ACTOR_BUILD_TIMED_OUT("ACTOR.BUILD.TIMED_OUT"), + /** A test event, sent when a webhook is tested. */ + TEST("TEST"), + /** An event type the API returned that this client version does not recognise. */ + UNKNOWN(null); + + private final String wireValue; + + WebhookEventType(String wireValue) { + this.wireValue = wireValue; + } + + /** + * The value used on the wire (e.g. {@code "ACTOR.RUN.SUCCEEDED"}); {@code null} for {@link + * #UNKNOWN}. + */ + @JsonValue + public String getWireValue() { + return wireValue; + } + + /** Maps a wire value to its constant, or {@link #UNKNOWN} for an unrecognised non-null value. */ + @JsonCreator + public static WebhookEventType fromWire(String value) { + if (value == null) { + return null; + } + for (WebhookEventType eventType : values()) { + if (value.equals(eventType.wireValue)) { + return eventType; + } + } + return UNKNOWN; + } +} diff --git a/src/test/java/com/apify/client/ReviewFixesTest.java b/src/test/java/com/apify/client/ReviewFixesTest.java index 3c17be2..01053f8 100644 --- a/src/test/java/com/apify/client/ReviewFixesTest.java +++ b/src/test/java/com/apify/client/ReviewFixesTest.java @@ -44,7 +44,7 @@ void lastRunForwardsStatusFilterToNestedStorages() { MockBackend ds = MockBackend.ofConstant(200, "[]"); client(ds) .actor("me/x") - .lastRun("SUCCEEDED") + .lastRun(RunStatus.SUCCEEDED) .dataset() .listItems(new DatasetListItemsOptions()); assertTrue(ds.lastUrl.contains("runs/last/dataset/items"), ds.lastUrl); @@ -53,7 +53,7 @@ void lastRunForwardsStatusFilterToNestedStorages() { MockBackend log = MockBackend.ofConstant(200, "log-text"); client(log) .actor("me/x") - .lastRun(new LastRunOptions().status("SUCCEEDED").origin("API")) + .lastRun(new LastRunOptions().status(RunStatus.SUCCEEDED).origin(RunOrigin.API)) .log() .get(); assertTrue(log.lastUrl.contains("runs/last/log"), log.lastUrl); @@ -267,7 +267,7 @@ void storeIterateDoesNotMutateCallerOptionsAndHonorsOffset() { MockBackend.ofConstant( 200, "{\"data\":{\"items\":[],\"total\":0,\"offset\":0,\"limit\":0,\"count\":0}}"); StoreListOptions options = new StoreListOptions().offset(100L).limit(50L); - client(backend).store().iterate(options).hasNext(); + client(backend).store().iterate(options).findFirst(); // The caller's initial offset must be honored for paging and left untouched afterwards. assertTrue(backend.lastUrl.contains("offset=100"), backend.lastUrl); assertEquals(100L, options.offsetValue(), "iteration must not mutate the caller's options"); @@ -284,13 +284,7 @@ void storeIterateWalksMultiplePages() { MockBackend.ok( 200, "{\"data\":{\"items\":[{}],\"total\":3,\"offset\":2,\"limit\":2,\"count\":1}}"))); - java.util.Iterator it = - client(backend).store().iterate(new StoreListOptions().limit(2L)); - int count = 0; - while (it.hasNext()) { - it.next(); - count++; - } + long count = client(backend).store().iterate(new StoreListOptions().limit(2L)).count(); assertEquals(3, count, "iteration should walk across both pages"); assertEquals(2, backend.calls, "one API call per page"); } @@ -381,7 +375,7 @@ void waitForFinishReturnsWhenResourceAppearsAfter404() { MockBackend.ok(200, "{\"data\":{\"id\":\"r1\",\"status\":\"SUCCEEDED\"}}"))); ActorRun run = client(backend).run("r1").waitForFinish(5L); assertEquals("r1", run.getId()); - assertEquals("SUCCEEDED", run.getStatus()); + assertEquals(RunStatus.SUCCEEDED, run.getStatus()); assertEquals(2, backend.calls, "one 404 poll then the terminal poll"); } diff --git a/src/test/java/com/apify/client/examples/IterateStore.java b/src/test/java/com/apify/client/examples/IterateStore.java index c1946c7..865fce3 100644 --- a/src/test/java/com/apify/client/examples/IterateStore.java +++ b/src/test/java/com/apify/client/examples/IterateStore.java @@ -1,9 +1,7 @@ package com.apify.client.examples; -import com.apify.client.ActorStoreListItem; import com.apify.client.ApifyClient; import com.apify.client.StoreListOptions; -import java.util.Iterator; /** * Lazily iterates over Actors in the Apify Store using the convenience iteration method, printing @@ -15,12 +13,14 @@ private IterateStore() {} public static void main(String[] args) { ApifyClient client = ApifyClient.create(System.getenv("APIFY_TOKEN")); - Iterator it = client.store().iterate(new StoreListOptions().limit(10L)); - int count = 0; - while (count < 5 && it.hasNext()) { - ActorStoreListItem item = it.next(); - System.out.println((count + 1) + ". " + item.getUsername() + "/" + item.getName()); - count++; - } + int[] index = {0}; + client + .store() + .iterate(new StoreListOptions().limit(10L)) + .limit(5) + .forEach( + item -> + System.out.println( + (++index[0]) + ". " + item.getUsername() + "/" + item.getName())); } } diff --git a/src/test/java/com/apify/client/examples/RunAndLastRunStorages.java b/src/test/java/com/apify/client/examples/RunAndLastRunStorages.java index 6f49757..2da8ed9 100644 --- a/src/test/java/com/apify/client/examples/RunAndLastRunStorages.java +++ b/src/test/java/com/apify/client/examples/RunAndLastRunStorages.java @@ -4,6 +4,7 @@ import com.apify.client.ActorStartOptions; import com.apify.client.ApifyClient; import com.apify.client.DatasetListItemsOptions; +import com.apify.client.RunStatus; import java.util.Optional; /** @@ -18,7 +19,8 @@ public static void main(String[] args) { client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L); - Optional lastRun = client.actor("apify/hello-world").lastRun("SUCCEEDED").get(); + Optional lastRun = + client.actor("apify/hello-world").lastRun(RunStatus.SUCCEEDED).get(); if (lastRun.isEmpty()) { throw new IllegalStateException("no succeeded run found"); } diff --git a/src/test/java/com/apify/client/integration/ActorRunIntegrationTest.java b/src/test/java/com/apify/client/integration/ActorRunIntegrationTest.java index 6bcb9ce..3158431 100644 --- a/src/test/java/com/apify/client/integration/ActorRunIntegrationTest.java +++ b/src/test/java/com/apify/client/integration/ActorRunIntegrationTest.java @@ -9,6 +9,8 @@ import com.apify.client.LastRunOptions; import com.apify.client.ListOptions; import com.apify.client.RunListOptions; +import com.apify.client.RunOrigin; +import com.apify.client.RunStatus; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -25,7 +27,7 @@ void listRuns() { void runActorAndReadOutputs() { ApifyClient client = requireClient(); ActorRun run = client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L); - assertEquals("SUCCEEDED", run.getStatus()); + assertEquals(RunStatus.SUCCEEDED, run.getStatus()); assertTrue(client.run(run.getId()).get().isPresent()); @@ -41,16 +43,16 @@ void lastRunAccess() { ApifyClient client = requireClient(); client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L); - var lastRun = client.actor("apify/hello-world").lastRun("SUCCEEDED").get(); + var lastRun = client.actor("apify/hello-world").lastRun(RunStatus.SUCCEEDED).get(); assertTrue(lastRun.isPresent()); - assertEquals("SUCCEEDED", lastRun.get().getStatus()); + assertEquals(RunStatus.SUCCEEDED, lastRun.get().getStatus()); var byOrigin = client .actor("apify/hello-world") - .lastRun(new LastRunOptions().status("SUCCEEDED").origin("API")) + .lastRun(new LastRunOptions().status(RunStatus.SUCCEEDED).origin(RunOrigin.API)) .get(); assertTrue(byOrigin.isPresent()); - assertEquals("SUCCEEDED", byOrigin.get().getStatus()); + assertEquals(RunStatus.SUCCEEDED, byOrigin.get().getStatus()); } } diff --git a/src/test/java/com/apify/client/integration/RequestQueueIntegrationTest.java b/src/test/java/com/apify/client/integration/RequestQueueIntegrationTest.java index 69277f2..4ce54bc 100644 --- a/src/test/java/com/apify/client/integration/RequestQueueIntegrationTest.java +++ b/src/test/java/com/apify/client/integration/RequestQueueIntegrationTest.java @@ -13,7 +13,6 @@ import com.apify.client.StorageListOptions; import java.util.ArrayList; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -77,10 +76,7 @@ void requestQueuePaginateMultiplePages() { queue.addRequest(new RequestQueueRequest(url, url), false); } Set seen = new HashSet<>(); - Iterator it = queue.paginateRequests(2L); - while (it.hasNext()) { - seen.add(it.next().getUrl()); - } + queue.paginateRequests(2L).forEach(request -> seen.add(request.getUrl())); assertEquals(total, seen.size()); } finally { client.requestQueue(rq.getId()).delete(); diff --git a/src/test/java/com/apify/client/integration/StoreIntegrationTest.java b/src/test/java/com/apify/client/integration/StoreIntegrationTest.java index 08d6abd..7fd4e2c 100644 --- a/src/test/java/com/apify/client/integration/StoreIntegrationTest.java +++ b/src/test/java/com/apify/client/integration/StoreIntegrationTest.java @@ -5,7 +5,8 @@ import com.apify.client.ActorStoreListItem; import com.apify.client.ApifyClient; import com.apify.client.StoreListOptions; -import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class StoreIntegrationTest extends IntegrationBase { @@ -20,13 +21,16 @@ void listStore() { @Test void iterateStore() { ApifyClient client = requireClient(); - Iterator it = client.store().iterate(new StoreListOptions().limit(5L)); - int count = 0; - while (count < 12 && it.hasNext()) { - ActorStoreListItem item = it.next(); + List items = + client + .store() + .iterate(new StoreListOptions().limit(5L)) + .limit(12) + .collect(Collectors.toList()); + for (ActorStoreListItem item : items) { assertTrue(item.getId() != null && !item.getId().isEmpty()); - count++; } - assertTrue(count >= 12, "expected to iterate at least 12 store actors, got " + count); + assertTrue( + items.size() >= 12, "expected to iterate at least 12 store actors, got " + items.size()); } } From 31244742bf08f4c81832dd30a1991592d5d778fe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 11:29:25 +0000 Subject: [PATCH 2/5] fix: address review of idiomatic refactor (version pins, enum edge cases, docs, tests) - README install snippets bumped to 0.2.0. - RunListOptions.apply filters null wire values so RunStatus.UNKNOWN cannot leak into the query. - PermissionLevel: drop unused @JsonValue (write-only enum), document intent. - docs/webhooks.md: document typed-getter vs string-create asymmetry; derive create keys via getWireValue(). - CHANGELOG: add abort(null) -> abort() migration hint. - Add EnumTest (UNKNOWN fallback, hyphenated/dotted wire values, end-to-end deserialization); wire into CI unit-test step. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JvkiV9GQnM5qo4iAnX43K7 --- .github/workflows/java-integration-tests.yml | 2 +- CHANGELOG.md | 2 +- README.md | 4 +- docs/webhooks.md | 12 +- .../com/apify/client/PermissionLevel.java | 6 +- .../java/com/apify/client/RunListOptions.java | 7 +- src/test/java/com/apify/client/EnumTest.java | 113 ++++++++++++++++++ 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/apify/client/EnumTest.java diff --git a/.github/workflows/java-integration-tests.yml b/.github/workflows/java-integration-tests.yml index 51160d9..0c6bfec 100644 --- a/.github/workflows/java-integration-tests.yml +++ b/.github/workflows/java-integration-tests.yml @@ -47,7 +47,7 @@ jobs: # Offline unit tests (mock HTTP backend): prove the retry/error/signature logic without the API. - name: Unit tests - run: mvn -B test -Dtest='UnitHttpTest,ReviewFixesTest,ClientMetaTest,SignatureTest,ConfigTest' -DfailIfNoTests=true + run: mvn -B test -Dtest='UnitHttpTest,ReviewFixesTest,ClientMetaTest,SignatureTest,ConfigTest,EnumTest' -DfailIfNoTests=true # Fail fast if the integration-test secret is missing or empty. Without this guard the # integration tests silently "pass" (JUnit assumptions skip them when APIFY_TOKEN is unset), diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3ec9c..758820e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Idiomatic-Java refactor. This release contains breaking changes to the public in - `StoreCollectionClient.iterate(...)` and `RequestQueueClient.paginateRequests(...)` now return a lazy `Stream` instead of an `Iterator`. - `RunClient.abort(Boolean)` is replaced by the overloads `abort()` (server default) and - `abort(boolean gracefully)`. + `abort(boolean gracefully)`. Migration: replace `abort(null)` with `abort()`. ### Added diff --git a/README.md b/README.md index 95fdd5d..a1c7765 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ Maven: com.apify apify-client - 0.1.1 + 0.2.0 ``` Gradle: ```groovy -implementation 'com.apify:apify-client:0.1.1' +implementation 'com.apify:apify-client:0.2.0' ``` ## Quick start diff --git a/docs/webhooks.md b/docs/webhooks.md index 4eba06a..39c535f 100644 --- a/docs/webhooks.md +++ b/docs/webhooks.md @@ -26,14 +26,18 @@ Actor or task, use `client.webhooks().create(...)` and set the Actor/task in the A webhook definition supplies `eventTypes` (the events that trigger it, such as `ACTOR.RUN.SUCCEEDED`, `ACTOR.RUN.FAILED`, `ACTOR.RUN.ABORTED`, `ACTOR.RUN.TIMED_OUT`, `ACTOR.RUN.CREATED`, `ACTOR.BUILD.SUCCEEDED`, …), a `condition` and a `requestUrl`. The definition you -pass to `create(...)` is a plain JSON-serializable value (e.g. a `Map`), so its event types are wire -strings; when reading a webhook back, `Webhook.getEventTypes()` returns them as a typed -`WebhookEventType` enum (with `UNKNOWN` for any value this client version does not recognise): +pass to `create(...)` is a plain JSON-serializable value (e.g. a `Map`), so on create the event types +must be their wire strings (e.g. `"ACTOR.RUN.SUCCEEDED"`). When reading a webhook back, +`Webhook.getEventTypes()` returns them as a typed `WebhookEventType` enum (with `UNKNOWN` for any value +this client version does not recognise). To stay type-safe on create, derive the wire strings from the +enum with `WebhookEventType.getWireValue()`: ```java Webhook webhook = client.webhooks().create(Map.of( "isAdHoc", false, - "eventTypes", List.of("ACTOR.RUN.SUCCEEDED", "ACTOR.RUN.FAILED"), + "eventTypes", List.of( + WebhookEventType.ACTOR_RUN_SUCCEEDED.getWireValue(), + WebhookEventType.ACTOR_RUN_FAILED.getWireValue()), "condition", Map.of("actorId", "apify/hello-world"), "requestUrl", "https://example.com/webhook")); ``` diff --git a/src/main/java/com/apify/client/PermissionLevel.java b/src/main/java/com/apify/client/PermissionLevel.java index 2c11bbe..abe0933 100644 --- a/src/main/java/com/apify/client/PermissionLevel.java +++ b/src/main/java/com/apify/client/PermissionLevel.java @@ -1,10 +1,11 @@ package com.apify.client; -import com.fasterxml.jackson.annotation.JsonValue; - /** * The permission level an Actor run executes with, used to override the Actor's default via {@link * ActorStartOptions#forcePermissionLevel(PermissionLevel)}. + * + *

This enum is write-only: it is only ever turned into the {@code forcePermissionLevel} query + * parameter via {@link #getWireValue()}, never deserialized, so it needs no Jackson annotations. */ public enum PermissionLevel { /** The run is restricted to a limited set of permissions. */ @@ -19,7 +20,6 @@ public enum PermissionLevel { } /** The value sent in the {@code forcePermissionLevel} query parameter. */ - @JsonValue public String getWireValue() { return wireValue; } diff --git a/src/main/java/com/apify/client/RunListOptions.java b/src/main/java/com/apify/client/RunListOptions.java index 439d231..bc58a75 100644 --- a/src/main/java/com/apify/client/RunListOptions.java +++ b/src/main/java/com/apify/client/RunListOptions.java @@ -1,6 +1,7 @@ package com.apify.client; import java.util.List; +import java.util.Objects; /** * Run-specific filters for {@link RunCollectionClient#list(ListOptions, RunListOptions)}. The @@ -34,8 +35,12 @@ public RunListOptions startedBefore(String startedBefore) { } void apply(QueryParams q) { + // Filter out null wire values (e.g. RunStatus.UNKNOWN, the read-only sentinel) so the + // parse-only fallback can never leak a literal "null" into the status query parameter. List statusValues = - status == null ? null : status.stream().map(RunStatus::getWireValue).toList(); + status == null + ? null + : status.stream().map(RunStatus::getWireValue).filter(Objects::nonNull).toList(); q.addCsv("status", statusValues) .addString("startedAfter", startedAfter) .addString("startedBefore", startedBefore); diff --git a/src/test/java/com/apify/client/EnumTest.java b/src/test/java/com/apify/client/EnumTest.java new file mode 100644 index 0000000..498dbd7 --- /dev/null +++ b/src/test/java/com/apify/client/EnumTest.java @@ -0,0 +1,113 @@ +package com.apify.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +/** + * Offline tests for the wire-value enums: the {@code fromWire} round-trip (including hyphenated and + * dotted wire values), the {@code UNKNOWN} fallback for unrecognised values, and the end-to-end + * Jackson deserialization path on the models that expose them. A future wire-value typo would flip + * a real value to {@code UNKNOWN} and fail these tests rather than degrade silently. + */ +class EnumTest { + + private static ApifyClient client(MockBackend backend) { + return ApifyClient.builder() + .token("test-token") + .httpBackend(backend) + .maxRetries(0) + .minDelayBetweenRetries(Duration.ofMillis(1)) + .build(); + } + + @Test + void runStatusRoundTripsAndFallsBack() { + // Every declared status (except the null-valued UNKNOWN sentinel) must round-trip its wire + // value. + for (RunStatus status : RunStatus.values()) { + if (status == RunStatus.UNKNOWN) { + continue; + } + assertEquals(status, RunStatus.fromWire(status.getWireValue())); + } + // Hyphenated wire values are the ones most likely to break on a rename. + assertEquals(RunStatus.TIMING_OUT, RunStatus.fromWire("TIMING-OUT")); + assertEquals(RunStatus.TIMED_OUT, RunStatus.fromWire("TIMED-OUT")); + assertEquals(RunStatus.UNKNOWN, RunStatus.fromWire("SOMETHING-NEW")); + assertNull(RunStatus.fromWire(null)); + assertNull(RunStatus.UNKNOWN.getWireValue()); + } + + @Test + void runStatusTerminality() { + assertTrue(RunStatus.SUCCEEDED.isTerminal()); + assertTrue(RunStatus.FAILED.isTerminal()); + assertTrue(RunStatus.ABORTED.isTerminal()); + assertTrue(RunStatus.TIMED_OUT.isTerminal()); + assertFalse(RunStatus.RUNNING.isTerminal()); + assertFalse(RunStatus.TIMING_OUT.isTerminal()); + assertFalse(RunStatus.UNKNOWN.isTerminal()); + } + + @Test + void runOriginRoundTripsAndFallsBack() { + for (RunOrigin origin : RunOrigin.values()) { + if (origin == RunOrigin.UNKNOWN) { + continue; + } + assertEquals(origin, RunOrigin.fromWire(origin.getWireValue())); + } + assertEquals(RunOrigin.UNKNOWN, RunOrigin.fromWire("FUTURE-ORIGIN")); + assertNull(RunOrigin.fromWire(null)); + } + + @Test + void webhookEventTypeRoundTripsDottedValuesAndFallsBack() { + for (WebhookEventType eventType : WebhookEventType.values()) { + if (eventType == WebhookEventType.UNKNOWN) { + continue; + } + assertEquals(eventType, WebhookEventType.fromWire(eventType.getWireValue())); + } + assertEquals( + WebhookEventType.ACTOR_RUN_SUCCEEDED, WebhookEventType.fromWire("ACTOR.RUN.SUCCEEDED")); + assertEquals(WebhookEventType.UNKNOWN, WebhookEventType.fromWire("ACTOR.RUN.MIGRATED")); + assertNull(WebhookEventType.fromWire(null)); + } + + @Test + void actorRunDeserializesStatusEnumEndToEnd() { + MockBackend terminal = + MockBackend.ofConstant(200, "{\"data\":{\"id\":\"r1\",\"status\":\"TIMED-OUT\"}}"); + Optional run = client(terminal).run("r1").get(); + assertTrue(run.isPresent()); + assertEquals(RunStatus.TIMED_OUT, run.get().getStatus()); + assertTrue(run.get().isTerminal()); + + MockBackend unknown = + MockBackend.ofConstant(200, "{\"data\":{\"id\":\"r2\",\"status\":\"WARP-DRIVE\"}}"); + ActorRun degraded = client(unknown).run("r2").get().orElseThrow(); + assertEquals(RunStatus.UNKNOWN, degraded.getStatus()); + assertFalse(degraded.isTerminal()); + } + + @Test + void webhookDeserializesEventTypeEnumsEndToEnd() { + MockBackend backend = + MockBackend.ofConstant( + 200, + "{\"data\":{\"id\":\"w1\",\"eventTypes\":[\"ACTOR.RUN.SUCCEEDED\",\"ACTOR.RUN.MIGRATED\"]}}"); + Optional webhook = client(backend).webhook("w1").get(); + assertTrue(webhook.isPresent()); + assertEquals( + List.of(WebhookEventType.ACTOR_RUN_SUCCEEDED, WebhookEventType.UNKNOWN), + webhook.get().getEventTypes()); + } +} From 124ec17ffc6f9ec2a849cc952450c94c69ec657a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 11:35:05 +0000 Subject: [PATCH 3/5] docs: bump stale client version in README Versioning section to 0.2.0 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JvkiV9GQnM5qo4iAnX43K7 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1c7765..b68ba1b 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ try { ## Versioning -- `Version.CLIENT_VERSION` — the semantic version of this client (`0.1.1`). +- `Version.CLIENT_VERSION` — the semantic version of this client (`0.2.0`). - `Version.API_SPEC_VERSION` — the Apify OpenAPI specification version this client was verified against (`v2-2026-07-07T132551Z`). From a469c10bf3301019bb20791e27d94904549f9b70 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 11:52:05 +0000 Subject: [PATCH 4/5] test: cover abort overloads and RunListOptions UNKNOWN filter; publish-gate EnumTest parity - Add EnumTest to the java-publish.yml unit-test step so the enum-correctness guard runs before release (parity with the PR gate). - Add offline unit tests: abort() omits the gracefully param, abort(true)/(false) map to gracefully=1/0, and RunListOptions filters RunStatus.UNKNOWN off the wire. - CHANGELOG: note nullable-Boolean auto-unboxing NPE risk in the abort migration. - Fix duplicated 'immediate' in RunClient.abort() javadoc. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JvkiV9GQnM5qo4iAnX43K7 --- .github/workflows/java-publish.yml | 2 +- CHANGELOG.md | 5 ++- src/main/java/com/apify/client/RunClient.java | 2 +- .../com/apify/client/ReviewFixesTest.java | 41 +++++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.github/workflows/java-publish.yml b/.github/workflows/java-publish.yml index 4ad8af1..4e1d770 100644 --- a/.github/workflows/java-publish.yml +++ b/.github/workflows/java-publish.yml @@ -63,7 +63,7 @@ jobs: run: mvn -B -DskipTests compile spotbugs:check - name: Unit tests - run: mvn -B test -Dtest='UnitHttpTest,ReviewFixesTest,ClientMetaTest,SignatureTest,ConfigTest' -DfailIfNoTests=true + run: mvn -B test -Dtest='UnitHttpTest,ReviewFixesTest,ClientMetaTest,SignatureTest,ConfigTest,EnumTest' -DfailIfNoTests=true - name: Resolve version from pom.xml id: version diff --git a/CHANGELOG.md b/CHANGELOG.md index 758820e..a35abc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,10 @@ Idiomatic-Java refactor. This release contains breaking changes to the public in - `StoreCollectionClient.iterate(...)` and `RequestQueueClient.paginateRequests(...)` now return a lazy `Stream` instead of an `Iterator`. - `RunClient.abort(Boolean)` is replaced by the overloads `abort()` (server default) and - `abort(boolean gracefully)`. Migration: replace `abort(null)` with `abort()`. + `abort(boolean gracefully)`. Migration: replace `abort(null)` with `abort()`. Callers passing a + nullable `Boolean` variable are also affected — it now auto-unboxes to `abort(boolean)` and throws + a `NullPointerException` on `null` instead of falling through to the server default; use `abort()` + when the value is absent. ### Added diff --git a/src/main/java/com/apify/client/RunClient.java b/src/main/java/com/apify/client/RunClient.java index b8ea091..4ffb6a5 100644 --- a/src/main/java/com/apify/client/RunClient.java +++ b/src/main/java/com/apify/client/RunClient.java @@ -68,7 +68,7 @@ public void delete() { ctx.deleteResource(""); } - /** Aborts the run immediately, applying the server default (an immediate abort). */ + /** Aborts the run, applying the server default (an immediate abort). */ public ActorRun abort() { return abortInternal(null); } diff --git a/src/test/java/com/apify/client/ReviewFixesTest.java b/src/test/java/com/apify/client/ReviewFixesTest.java index 01053f8..31c0377 100644 --- a/src/test/java/com/apify/client/ReviewFixesTest.java +++ b/src/test/java/com/apify/client/ReviewFixesTest.java @@ -398,4 +398,45 @@ void logStreamThrowsOnNonSuccessStatus() { assertThrows(ApifyApiException.class, () -> client(backend).log("run1").stream()); assertEquals(403, ex.getStatusCode()); } + + @Test + void abortNoArgOmitsGracefullyParam() { + // abort() applies the server default, so the gracefully query param must not be sent at all. + MockBackend backend = + MockBackend.ofConstant(200, "{\"data\":{\"id\":\"r1\",\"status\":\"ABORTING\"}}"); + ActorRun run = client(backend).run("r1").abort(); + assertEquals("r1", run.getId()); + assertTrue(backend.lastUrl.contains("/actor-runs/r1/abort"), backend.lastUrl); + assertFalse(backend.lastUrl.contains("gracefully="), backend.lastUrl); + } + + @Test + void abortGracefullyTrueMapsToOne() { + MockBackend backend = + MockBackend.ofConstant(200, "{\"data\":{\"id\":\"r1\",\"status\":\"ABORTING\"}}"); + client(backend).run("r1").abort(true); + assertTrue(backend.lastUrl.contains("gracefully=1"), backend.lastUrl); + } + + @Test + void abortGracefullyFalseMapsToZero() { + MockBackend backend = + MockBackend.ofConstant(200, "{\"data\":{\"id\":\"r1\",\"status\":\"ABORTING\"}}"); + client(backend).run("r1").abort(false); + assertTrue(backend.lastUrl.contains("gracefully=0"), backend.lastUrl); + } + + @Test + void runListOptionsFiltersUnknownStatusFromWire() { + // RunStatus.UNKNOWN is a read-only parse sentinel with a null wire value; it must be dropped + // from the status filter so a literal "status=null" can never reach the query string. + MockBackend backend = + MockBackend.ofConstant( + 200, "{\"data\":{\"items\":[],\"total\":0,\"offset\":0,\"limit\":0,\"count\":0}}"); + client(backend) + .runs() + .list(null, new RunListOptions().status(List.of(RunStatus.SUCCEEDED, RunStatus.UNKNOWN))); + assertTrue(backend.lastUrl.contains("status=SUCCEEDED"), backend.lastUrl); + assertFalse(backend.lastUrl.contains("null"), backend.lastUrl); + } } From a59c6639fb527a5990e672d9d142eaf5ef2e7501 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 11:55:25 +0000 Subject: [PATCH 5/5] refactor: make RunOrigin write-only, matching PermissionLevel RunOrigin is only emitted to the origin query param via LastRunOptions; no model field deserializes it, so its @JsonCreator/@JsonValue/UNKNOWN machinery was dead and the 'deserialization never fails' javadoc described a nonexistent path. Strip the annotations and UNKNOWN sentinel and document it as write-only, mirroring the refactor's own treatment of PermissionLevel. EnumTest updated accordingly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JvkiV9GQnM5qo4iAnX43K7 --- src/main/java/com/apify/client/RunOrigin.java | 32 ++++--------------- src/test/java/com/apify/client/EnumTest.java | 14 +++----- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/apify/client/RunOrigin.java b/src/main/java/com/apify/client/RunOrigin.java index 2b7cb19..b811882 100644 --- a/src/main/java/com/apify/client/RunOrigin.java +++ b/src/main/java/com/apify/client/RunOrigin.java @@ -1,13 +1,12 @@ package com.apify.client; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - /** - * How an Actor run was started (the API's {@code RunOrigin} / {@code meta.origin}). + * How an Actor run was started (the API's {@code RunOrigin} / {@code meta.origin}), used to filter + * the "last" run via {@link LastRunOptions#origin(RunOrigin)}. * - *

{@link #UNKNOWN} is returned for any origin value the API introduces that this client version - * does not yet recognise, so deserialization never fails on a newer server. + *

This enum is write-only: it is only ever turned into the {@code origin} query parameter via + * {@link #getWireValue()}, never deserialized, so it needs no Jackson annotations and no {@code + * UNKNOWN} read sentinel. */ public enum RunOrigin { /** Started from the Actor's development console. */ @@ -31,9 +30,7 @@ public enum RunOrigin { /** Started from an Actor Standby request. */ STANDBY("STANDBY"), /** Started through the Model Context Protocol integration. */ - MCP("MCP"), - /** An origin value the API returned that this client version does not recognise. */ - UNKNOWN(null); + MCP("MCP"); private final String wireValue; @@ -41,23 +38,8 @@ public enum RunOrigin { this.wireValue = wireValue; } - /** The value used on the wire; {@code null} for {@link #UNKNOWN}. */ - @JsonValue + /** The value sent in the {@code origin} query parameter. */ public String getWireValue() { return wireValue; } - - /** Maps a wire value to its constant, or {@link #UNKNOWN} for an unrecognised non-null value. */ - @JsonCreator - public static RunOrigin fromWire(String value) { - if (value == null) { - return null; - } - for (RunOrigin origin : values()) { - if (value.equals(origin.wireValue)) { - return origin; - } - } - return UNKNOWN; - } } diff --git a/src/test/java/com/apify/client/EnumTest.java b/src/test/java/com/apify/client/EnumTest.java index 498dbd7..a1449a3 100644 --- a/src/test/java/com/apify/client/EnumTest.java +++ b/src/test/java/com/apify/client/EnumTest.java @@ -57,15 +57,11 @@ void runStatusTerminality() { } @Test - void runOriginRoundTripsAndFallsBack() { - for (RunOrigin origin : RunOrigin.values()) { - if (origin == RunOrigin.UNKNOWN) { - continue; - } - assertEquals(origin, RunOrigin.fromWire(origin.getWireValue())); - } - assertEquals(RunOrigin.UNKNOWN, RunOrigin.fromWire("FUTURE-ORIGIN")); - assertNull(RunOrigin.fromWire(null)); + void runOriginEmitsItsWireValue() { + // RunOrigin is write-only (only ever emitted to the origin query param), so it only needs a + // wire-value getter; there is no fromWire/UNKNOWN read path to exercise. + assertEquals("API", RunOrigin.API.getWireValue()); + assertEquals("SCHEDULER", RunOrigin.SCHEDULER.getWireValue()); } @Test