1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
//! Client for a single Actor (`/v2/actors/{actorId}`).
use serde::Serialize;
use serde_json::Value;
use crate::client::ApifyClient;
use crate::clients::actor_version::ActorVersionClient;
use crate::clients::actor_version_collection::ActorVersionCollectionClient;
use crate::clients::base::{
delete_resource, get_resource, post_with_body, update_resource, ResourceContext,
};
use crate::clients::build::BuildClient;
use crate::clients::build_collection::BuildCollectionClient;
use crate::clients::run::{LastRunOptions, RunClient};
use crate::clients::run_collection::RunCollectionClient;
use crate::clients::webhook_collection::WebhookCollectionClient;
use crate::common::{parse_data_envelope, QueryParams};
use crate::error::ApifyClientResult;
use crate::http_client::HttpClient;
use crate::models::{Actor, ActorRun, Build};
/// Options shared by [`ActorClient::start`] and [`ActorClient::call`] (and the task
/// equivalents).
#[derive(Debug, Default, Clone)]
pub struct ActorStartOptions {
/// Tag or number of the build to run (e.g. `latest`, `0.1.2`).
pub build: Option<String>,
/// Memory in megabytes allocated for the run.
pub memory_mbytes: Option<i64>,
/// Timeout for the run in seconds (`0` means no timeout).
pub timeout_secs: Option<i64>,
/// Maximum seconds to wait server-side for the run to finish (max 60).
pub wait_for_finish: Option<i64>,
/// Maximum number of dataset items to charge (pay-per-result Actors).
pub max_items: Option<i64>,
/// Maximum total charge in USD (pay-per-event Actors).
pub max_total_charge_usd: Option<f64>,
/// Content type of the input body. Defaults to `application/json`.
pub content_type: Option<String>,
/// Whether to restart the run if it fails.
pub restart_on_error: Option<bool>,
/// Override the Actor's permission level for this run.
pub force_permission_level: Option<String>,
/// Ad-hoc webhooks to attach to this run. Serialized to base64-encoded JSON as the
/// `webhooks` query parameter, matching the reference clients.
pub webhooks: Option<Vec<serde_json::Value>>,
}
impl ActorStartOptions {
/// Serializes these options into run-start query parameters. Shared by the Actor and
/// task start methods (DRY).
pub(crate) fn apply(&self, params: &mut QueryParams) {
params
.add_str("build", self.build.clone())
.add_int("memory", self.memory_mbytes)
.add_int("timeout", self.timeout_secs)
.add_int("waitForFinish", self.wait_for_finish)
.add_int("maxItems", self.max_items)
.add_float("maxTotalChargeUsd", self.max_total_charge_usd)
.add_bool("restartOnError", self.restart_on_error)
.add_str("forcePermissionLevel", self.force_permission_level.clone())
.add_str("webhooks", self.encoded_webhooks());
}
/// Encodes the `webhooks` array as base64-encoded JSON, as required by the API.
fn encoded_webhooks(&self) -> Option<String> {
use base64::Engine;
let webhooks = self.webhooks.as_ref()?;
let json = serde_json::to_vec(webhooks).ok()?;
Some(base64::engine::general_purpose::STANDARD.encode(json))
}
}
/// Options for building an Actor.
#[derive(Debug, Default, Clone)]
pub struct ActorBuildOptions {
/// If `true`, use beta versions of Apify packages.
pub beta_packages: Option<bool>,
/// Tag to apply to the build (e.g. `latest`).
pub tag: Option<String>,
/// Whether to use the Docker build cache (default `true`).
pub use_cache: Option<bool>,
/// Maximum seconds to wait server-side for the build to finish (max 60).
pub wait_for_finish: Option<i64>,
}
/// Client for a specific Actor.
///
/// Provides CRUD methods plus convenience helpers to start/call the Actor, build it,
/// and access its runs, builds, versions and webhooks.
#[derive(Debug, Clone)]
pub struct ActorClient {
root: ApifyClient,
ctx: ResourceContext,
base_url: String,
id: String,
}
impl ActorClient {
pub(crate) fn new(root: ApifyClient, http: HttpClient, base_url: &str, id: &str) -> Self {
Self {
root,
ctx: ResourceContext::single(http, base_url, "actors", id),
base_url: base_url.to_string(),
id: id.to_string(),
}
}
/// Fetches the Actor object, or `None` if it does not exist.
pub async fn get(&self) -> ApifyClientResult<Option<Actor>> {
get_resource(&self.ctx, None, &QueryParams::new()).await
}
/// Updates the Actor with the given fields and returns the updated object.
pub async fn update<T: Serialize>(&self, new_fields: &T) -> ApifyClientResult<Actor> {
update_resource(&self.ctx, None, new_fields).await
}
/// Deletes the Actor.
pub async fn delete(&self) -> ApifyClientResult<()> {
delete_resource(&self.ctx, None).await
}
/// Starts the Actor and returns immediately with the created run.
///
/// `input` is any JSON-serializable value (or `None` for no input).
pub async fn start<T: Serialize>(
&self,
input: Option<&T>,
options: ActorStartOptions,
) -> ApifyClientResult<ActorRun> {
let mut params = QueryParams::new();
options.apply(&mut params);
let content_type = options
.content_type
.clone()
.unwrap_or_else(|| "application/json".to_string());
let body = match input {
Some(value) => Some(serde_json::to_vec(value)?),
None => None,
};
post_with_body(&self.ctx, Some("runs"), ¶ms, body, &content_type).await
}
/// Starts the Actor and waits (client-side polling) for it to finish.
///
/// `wait_secs` controls the wait budget:
/// - `None` polls indefinitely until the run reaches a terminal state.
/// - `Some(n)` bounds the wait to roughly `n` seconds; if the run has not finished by
/// then, the **last fetched (still non-terminal) run is returned** rather than an
/// error. Check `status` / `is_terminal()` on the result when using `Some`.
pub async fn call<T: Serialize>(
&self,
input: Option<&T>,
options: ActorStartOptions,
wait_secs: Option<i64>,
) -> ApifyClientResult<ActorRun> {
let run = self.start(input, options).await?;
// Use the root client's run client so polling targets the canonical run route.
self.root.run(run.id).wait_for_finish(wait_secs).await
}
/// Builds the given version of the Actor and returns the created build.
pub async fn build(
&self,
version_number: &str,
options: ActorBuildOptions,
) -> ApifyClientResult<Build> {
let mut params = QueryParams::new();
params
.add_str("version", Some(version_number.to_string()))
.add_bool("betaPackages", options.beta_packages)
.add_str("tag", options.tag)
.add_bool("useCache", options.use_cache)
.add_int("waitForFinish", options.wait_for_finish);
post_with_body(&self.ctx, Some("builds"), ¶ms, None, "application/json").await
}
/// Resolves the Actor's default build and returns a client for it.
///
/// `wait_for_finish` optionally bounds how long (in seconds) the API waits for the build
/// to finish before responding, matching the reference client's `defaultBuild(options)`.
pub async fn default_build(
&self,
wait_for_finish: Option<i64>,
) -> ApifyClientResult<BuildClient> {
let mut params = QueryParams::new();
params.add_int("waitForFinish", wait_for_finish);
let url = params.apply_to_url(&self.ctx.url(Some("builds/default")));
let response = self
.ctx
.http
.call(crate::http_client::HttpRequest {
method: crate::http_client::HttpMethod::Get,
url,
headers: Default::default(),
body: None,
timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
})
.await?;
let build: Build = parse_data_envelope(&response.body)?;
Ok(BuildClient::new(
self.ctx.http.clone(),
&self.base_url,
&build.id,
))
}
/// Validates the given input against the Actor's input schema.
///
/// Uses the Actor's default build for the input schema. To validate against a specific
/// build, use [`ActorClient::validate_input_for_build`].
pub async fn validate_input<T: Serialize>(&self, input: &T) -> ApifyClientResult<Value> {
self.validate_input_for_build(input, None).await
}
/// Validates the given input against the input schema of a specific Actor build.
///
/// `build` is the optional tag or number of the Actor build whose input schema is used for
/// validation (e.g. `"latest"` or `"1.2.34"`); passing `None` uses the default build, which
/// is equivalent to [`ActorClient::validate_input`]. This maps to the spec's optional `build`
/// query parameter on `POST /v2/actors/{actorId}/validate-input`.
pub async fn validate_input_for_build<T: Serialize>(
&self,
input: &T,
build: Option<&str>,
) -> ApifyClientResult<Value> {
let body = serde_json::to_vec(input)?;
let mut params = QueryParams::new();
params.add_str("build", build);
// `validate-input` returns a bare `{ "valid": ... }` object, *not* the usual
// `{ "data": ... }` envelope, so it must skip `parse_data_envelope`.
crate::clients::base::post_action_raw(
&self.ctx,
Some("validate-input"),
¶ms,
Some(body),
Some("application/json"),
)
.await
}
/// Returns a client for the last run of this Actor, optionally filtered by run status.
///
/// `status` filters by run status (e.g. `"SUCCEEDED"`, `"FAILED"`, `"RUNNING"`); pass `None`
/// to leave it unfiltered. This maps to the `status` query parameter on
/// `GET /v2/actors/{actorId}/runs/last` and mirrors the reference client's `lastRun({ status })`.
/// To also filter by `origin`, use [`ActorClient::last_run_with_options`].
pub fn last_run(&self, status: Option<&str>) -> RunClient {
self.last_run_with_options(LastRunOptions {
status: status.map(str::to_owned),
origin: None,
})
}
/// Returns a client for the last run of this Actor, applying the given [`LastRunOptions`]
/// (e.g. [`LastRunOptions::status`] and/or [`LastRunOptions::origin`]).
///
/// `status` filters by run status (e.g. `"SUCCEEDED"`, `"FAILED"`, `"RUNNING"`) and is the
/// spec's documented filter on `GET /v2/actors/{actorId}/runs/last`. `origin` filters by how
/// the run was started; accepted values are the platform's run origins (e.g. `"DEVELOPMENT"`,
/// `"WEB"`, `"API"`, `"SCHEDULER"`). `origin` is not declared by the OpenAPI spec and is sent
/// only for parity with the reference client's `lastRun({ status, origin })`. Both are sent as
/// query parameters; leave a field as `None` to omit it.
pub fn last_run_with_options(&self, options: LastRunOptions) -> RunClient {
let mut client = RunClient::new(
self.root.clone(),
self.ctx.http.clone(),
&self.ctx.url(None),
"runs",
"last",
);
if let Some(status) = options.status.as_deref() {
client.set_base_param("status", status);
}
if let Some(origin) = options.origin.as_deref() {
client.set_base_param("origin", origin);
}
client
}
/// Returns a client for this Actor's build collection.
pub fn builds(&self) -> BuildCollectionClient {
BuildCollectionClient::with_base(self.ctx.http.clone(), &self.ctx.url(None), "builds")
}
/// Returns a client for this Actor's run collection.
pub fn runs(&self) -> RunCollectionClient {
RunCollectionClient::new(self.ctx.http.clone(), &self.ctx.url(None), "runs")
}
/// Returns a client for a specific version of this Actor.
pub fn version(&self, version_number: &str) -> ActorVersionClient {
ActorVersionClient::new(self.ctx.http.clone(), &self.ctx.url(None), version_number)
}
/// Returns a client for this Actor's version collection.
pub fn versions(&self) -> ActorVersionCollectionClient {
ActorVersionCollectionClient::new(self.ctx.http.clone(), &self.ctx.url(None))
}
/// Returns a client for this Actor's webhook collection.
pub fn webhooks(&self) -> WebhookCollectionClient {
WebhookCollectionClient::with_base(self.ctx.http.clone(), &self.ctx.url(None))
}
/// The Actor's ID (or `username~name`) as provided.
pub fn id(&self) -> &str {
&self.id
}
}