Skip to main content

apify_client/clients/
actor.rs

1//! Client for a single Actor (`/v2/actors/{actorId}`).
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::client::ApifyClient;
7use crate::clients::actor_version::ActorVersionClient;
8use crate::clients::actor_version_collection::ActorVersionCollectionClient;
9use crate::clients::base::{
10    delete_resource, get_resource, post_with_body, update_resource, ResourceContext,
11};
12use crate::clients::build::BuildClient;
13use crate::clients::build_collection::BuildCollectionClient;
14use crate::clients::run::{LastRunOptions, RunClient};
15use crate::clients::run_collection::RunCollectionClient;
16use crate::clients::webhook_collection::WebhookCollectionClient;
17use crate::common::{parse_data_envelope, QueryParams};
18use crate::error::ApifyClientResult;
19use crate::http_client::HttpClient;
20use crate::models::{Actor, ActorRun, Build};
21
22/// How long to keep retrying a run start that the API rejects for lack of resources (see
23/// [`ActorStartOptions::wait_for_resources`]).
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum WaitForResources {
26    /// Retry until the run starts, how ever long that takes.
27    Forever,
28    /// Retry for up to this many seconds (clamped to `0` if negative), then return the last
29    /// error. `0` makes a single attempt and returns its error without retrying.
30    ForSecs(f64),
31}
32
33/// Options shared by [`ActorClient::start`] and [`ActorClient::call`] (and the task
34/// equivalents).
35#[derive(Debug, Default, Clone)]
36pub struct ActorStartOptions {
37    /// Tag or number of the build to run (e.g. `latest`, `0.1.2`).
38    pub build: Option<String>,
39    /// Memory in megabytes allocated for the run.
40    pub memory_mbytes: Option<i64>,
41    /// Timeout for the run in seconds (`0` means no timeout).
42    pub timeout_secs: Option<i64>,
43    /// Maximum seconds to wait server-side for the run to finish (max 60).
44    pub wait_for_finish: Option<i64>,
45    /// Maximum number of dataset items to charge (pay-per-result Actors).
46    pub max_items: Option<i64>,
47    /// Maximum total charge in USD (pay-per-event Actors).
48    pub max_total_charge_usd: Option<f64>,
49    /// Content type of the input body. Defaults to `application/json`.
50    pub content_type: Option<String>,
51    /// Whether to restart the run if it fails.
52    pub restart_on_error: Option<bool>,
53    /// Override the Actor's permission level for this run.
54    pub force_permission_level: Option<String>,
55    /// Ad-hoc webhooks to attach to this run. Serialized to base64-encoded JSON as the
56    /// `webhooks` query parameter, matching the reference clients.
57    pub webhooks: Option<Vec<serde_json::Value>>,
58    /// Retry the start while the account lacks the resources for the run, i.e. while the API
59    /// rejects it with an [`ApiError`](crate::ApiError) whose `error_type` is
60    /// `actor-memory-limit-exceeded` or `concurrent-runs-limit-exceeded`. Both clear as other
61    /// runs or builds finish. The start is retried every 10 seconds; any other error is returned
62    /// right away.
63    ///
64    /// [`WaitForResources::Forever`] retries until the run starts.
65    /// [`WaitForResources::ForSecs`] stops retrying after that many seconds and returns the last
66    /// error. `None` (the default) makes a single attempt.
67    ///
68    /// A run that requests more memory than the account's whole memory limit is rejected with
69    /// `actor-memory-limit-exceeded` as well and never starts, so `Forever` retries it forever.
70    pub wait_for_resources: Option<WaitForResources>,
71}
72
73impl ActorStartOptions {
74    /// Serializes these options into run-start query parameters. Shared by the Actor and
75    /// task start methods (DRY).
76    pub(crate) fn apply(&self, params: &mut QueryParams) {
77        params
78            .add_str("build", self.build.clone())
79            .add_int("memory", self.memory_mbytes)
80            .add_int("timeout", self.timeout_secs)
81            .add_int("waitForFinish", self.wait_for_finish)
82            .add_int("maxItems", self.max_items)
83            .add_float("maxTotalChargeUsd", self.max_total_charge_usd)
84            .add_bool("restartOnError", self.restart_on_error)
85            .add_str("forcePermissionLevel", self.force_permission_level.clone())
86            .add_str("webhooks", self.encoded_webhooks());
87    }
88
89    /// Encodes the `webhooks` array as base64-encoded JSON, as required by the API.
90    fn encoded_webhooks(&self) -> Option<String> {
91        use base64::Engine;
92        let webhooks = self.webhooks.as_ref()?;
93        let json = serde_json::to_vec(webhooks).ok()?;
94        Some(base64::engine::general_purpose::STANDARD.encode(json))
95    }
96}
97
98/// Options for building an Actor.
99#[derive(Debug, Default, Clone)]
100pub struct ActorBuildOptions {
101    /// If `true`, use beta versions of Apify packages.
102    pub beta_packages: Option<bool>,
103    /// Tag to apply to the build (e.g. `latest`).
104    pub tag: Option<String>,
105    /// Whether to use the Docker build cache (default `true`).
106    pub use_cache: Option<bool>,
107    /// Maximum seconds to wait server-side for the build to finish (max 60).
108    pub wait_for_finish: Option<i64>,
109}
110
111/// Client for a specific Actor.
112///
113/// Provides CRUD methods plus convenience helpers to start/call the Actor, build it,
114/// and access its runs, builds, versions and webhooks.
115#[derive(Debug, Clone)]
116pub struct ActorClient {
117    root: ApifyClient,
118    ctx: ResourceContext,
119    base_url: String,
120    id: String,
121}
122
123impl ActorClient {
124    pub(crate) fn new(root: ApifyClient, http: HttpClient, base_url: &str, id: &str) -> Self {
125        Self {
126            root,
127            ctx: ResourceContext::single(http, base_url, "actors", id),
128            base_url: base_url.to_string(),
129            id: id.to_string(),
130        }
131    }
132
133    /// Fetches the Actor object, or `None` if it does not exist.
134    pub async fn get(&self) -> ApifyClientResult<Option<Actor>> {
135        get_resource(&self.ctx, None, &QueryParams::new()).await
136    }
137
138    /// Updates the Actor with the given fields and returns the updated object.
139    pub async fn update<T: Serialize>(&self, new_fields: &T) -> ApifyClientResult<Actor> {
140        update_resource(&self.ctx, None, new_fields).await
141    }
142
143    /// Deletes the Actor.
144    pub async fn delete(&self) -> ApifyClientResult<()> {
145        delete_resource(&self.ctx, None).await
146    }
147
148    /// Starts the Actor and returns immediately with the created run.
149    ///
150    /// `input` is any JSON-serializable value (or `None` for no input). To send a non-JSON
151    /// input (e.g. a ZIP archive) as raw bytes instead, use [`start_raw`](Self::start_raw).
152    /// Set [`ActorStartOptions::wait_for_resources`] to retry the start while the account lacks
153    /// the memory or a concurrent-run slot for it.
154    pub async fn start<T: Serialize>(
155        &self,
156        input: Option<&T>,
157        options: ActorStartOptions,
158    ) -> ApifyClientResult<ActorRun> {
159        let body = match input {
160            Some(value) => Some(serde_json::to_vec(value)?),
161            None => None,
162        };
163        self.start_with_body(body, "application/json", options)
164            .await
165    }
166
167    /// Starts the Actor with a raw request body instead of a JSON-serializable value.
168    ///
169    /// Use this for a non-JSON input, e.g. a ZIP archive paired with
170    /// `options.content_type = Some("application/zip".into())`; the bytes are sent exactly as
171    /// given, with no JSON serialization. For an object or array input, use
172    /// [`start`](Self::start) instead, which handles the serialization. Mirrors the reference
173    /// client, whose `ActorInput` accepts a plain object, an array of them, or raw bytes.
174    pub async fn start_raw(
175        &self,
176        input: &[u8],
177        options: ActorStartOptions,
178    ) -> ApifyClientResult<ActorRun> {
179        self.start_with_body(Some(input.to_vec()), "application/octet-stream", options)
180            .await
181    }
182
183    /// Shared implementation of [`start`](Self::start) and [`start_raw`](Self::start_raw):
184    /// applies the run-start options as query parameters and posts the given body, falling back
185    /// to `default_content_type` only when the caller did not set `options.content_type`.
186    async fn start_with_body(
187        &self,
188        body: Option<Vec<u8>>,
189        default_content_type: &str,
190        options: ActorStartOptions,
191    ) -> ApifyClientResult<ActorRun> {
192        let mut params = QueryParams::new();
193        options.apply(&mut params);
194        let content_type = options
195            .content_type
196            .clone()
197            .unwrap_or_else(|| default_content_type.to_string());
198        crate::clients::base::retry_while_resource_limited(options.wait_for_resources, || {
199            post_with_body(
200                &self.ctx,
201                Some("runs"),
202                &params,
203                body.clone(),
204                &content_type,
205            )
206        })
207        .await
208    }
209
210    /// Starts the Actor and waits (client-side polling) for it to finish.
211    ///
212    /// `wait_secs` controls the wait budget:
213    /// - `None` polls indefinitely until the run reaches a terminal state.
214    /// - `Some(n)` bounds the wait to roughly `n` seconds; if the run has not finished by
215    ///   then, the **last fetched (still non-terminal) run is returned** rather than an
216    ///   error. Check `status` / `is_terminal()` on the result when using `Some`.
217    ///
218    /// Time spent retrying under [`ActorStartOptions::wait_for_resources`] does not count
219    /// toward `wait_secs`, since that budget only starts once the run has actually started.
220    pub async fn call<T: Serialize>(
221        &self,
222        input: Option<&T>,
223        options: ActorStartOptions,
224        wait_secs: Option<i64>,
225    ) -> ApifyClientResult<ActorRun> {
226        let run = self.start(input, options).await?;
227        // Use the root client's run client so polling targets the canonical run route.
228        self.root.run(run.id).wait_for_finish(wait_secs).await
229    }
230
231    /// Starts the Actor with a raw request body ([`start_raw`](Self::start_raw)) and waits for
232    /// it to finish, exactly like [`call`](Self::call) but for a non-JSON input.
233    pub async fn call_raw(
234        &self,
235        input: &[u8],
236        options: ActorStartOptions,
237        wait_secs: Option<i64>,
238    ) -> ApifyClientResult<ActorRun> {
239        let run = self.start_raw(input, options).await?;
240        self.root.run(run.id).wait_for_finish(wait_secs).await
241    }
242
243    /// Builds the given version of the Actor and returns the created build.
244    pub async fn build(
245        &self,
246        version_number: &str,
247        options: ActorBuildOptions,
248    ) -> ApifyClientResult<Build> {
249        let mut params = QueryParams::new();
250        params
251            .add_str("version", Some(version_number.to_string()))
252            .add_bool("betaPackages", options.beta_packages)
253            .add_str("tag", options.tag)
254            .add_bool("useCache", options.use_cache)
255            .add_int("waitForFinish", options.wait_for_finish);
256        post_with_body(&self.ctx, Some("builds"), &params, None, "application/json").await
257    }
258
259    /// Resolves the Actor's default build and returns a client for it.
260    ///
261    /// `wait_for_finish` optionally bounds how long (in seconds) the API waits for the build
262    /// to finish before responding, matching the reference client's `defaultBuild(options)`.
263    pub async fn default_build(
264        &self,
265        wait_for_finish: Option<i64>,
266    ) -> ApifyClientResult<BuildClient> {
267        let mut params = QueryParams::new();
268        params.add_int("waitForFinish", wait_for_finish);
269        let url = params.apply_to_url(&self.ctx.url(Some("builds/default")));
270        let response = self
271            .ctx
272            .http
273            .call(crate::http_client::HttpRequest {
274                method: crate::http_client::HttpMethod::Get,
275                url,
276                headers: Default::default(),
277                body: None,
278                timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
279            })
280            .await?;
281        let build: Build = parse_data_envelope(&response.body)?;
282        Ok(BuildClient::new(
283            self.ctx.http.clone(),
284            &self.base_url,
285            &build.id,
286        ))
287    }
288
289    /// Validates the given input against the Actor's input schema.
290    ///
291    /// Uses the Actor's default build for the input schema. To validate against a specific
292    /// build, use [`ActorClient::validate_input_for_build`].
293    pub async fn validate_input<T: Serialize>(&self, input: &T) -> ApifyClientResult<Value> {
294        self.validate_input_for_build(input, None).await
295    }
296
297    /// Validates the given input against the input schema of a specific Actor build.
298    ///
299    /// `build` is the optional tag or number of the Actor build whose input schema is used for
300    /// validation (e.g. `"latest"` or `"1.2.34"`); passing `None` uses the default build, which
301    /// is equivalent to [`ActorClient::validate_input`]. This maps to the spec's optional `build`
302    /// query parameter on `POST /v2/actors/{actorId}/validate-input`.
303    pub async fn validate_input_for_build<T: Serialize>(
304        &self,
305        input: &T,
306        build: Option<&str>,
307    ) -> ApifyClientResult<Value> {
308        let body = serde_json::to_vec(input)?;
309        let mut params = QueryParams::new();
310        params.add_str("build", build);
311        // `validate-input` returns a bare `{ "valid": ... }` object, *not* the usual
312        // `{ "data": ... }` envelope, so it must skip `parse_data_envelope`.
313        crate::clients::base::post_action_raw(
314            &self.ctx,
315            Some("validate-input"),
316            &params,
317            Some(body),
318            Some("application/json"),
319        )
320        .await
321    }
322
323    /// Returns a client for the last run of this Actor, optionally filtered by run status.
324    ///
325    /// `status` filters by run status (e.g. `"SUCCEEDED"`, `"FAILED"`, `"RUNNING"`); pass `None`
326    /// to leave it unfiltered. This maps to the `status` query parameter on
327    /// `GET /v2/actors/{actorId}/runs/last` and mirrors the reference client's `lastRun({ status })`.
328    /// To also filter by `origin`, use [`ActorClient::last_run_with_options`].
329    pub fn last_run(&self, status: Option<&str>) -> RunClient {
330        self.last_run_with_options(LastRunOptions {
331            status: status.map(str::to_owned),
332            origin: None,
333        })
334    }
335
336    /// Returns a client for the last run of this Actor, applying the given [`LastRunOptions`]
337    /// (e.g. [`LastRunOptions::status`] and/or [`LastRunOptions::origin`]).
338    ///
339    /// `status` filters by run status (e.g. `"SUCCEEDED"`, `"FAILED"`, `"RUNNING"`); `origin` filters
340    /// by how the run was started, with accepted values being the platform's run origins (e.g.
341    /// `"DEVELOPMENT"`, `"WEB"`, `"API"`, `"SCHEDULER"`). Both are documented optional query
342    /// parameters on `GET /v2/actors/{actorId}/runs/last` and match the reference client's
343    /// `lastRun({ status, origin })`; leave a field as `None` to omit it.
344    pub fn last_run_with_options(&self, options: LastRunOptions) -> RunClient {
345        let mut client = RunClient::new(
346            self.root.clone(),
347            self.ctx.http.clone(),
348            &self.ctx.url(None),
349            "runs",
350            "last",
351        );
352        if let Some(status) = options.status.as_deref() {
353            client.set_base_param("status", status);
354        }
355        if let Some(origin) = options.origin.as_deref() {
356            client.set_base_param("origin", origin);
357        }
358        client
359    }
360
361    /// Returns a client for this Actor's build collection.
362    pub fn builds(&self) -> BuildCollectionClient {
363        BuildCollectionClient::with_base(self.ctx.http.clone(), &self.ctx.url(None), "builds")
364    }
365
366    /// Returns a client for this Actor's run collection.
367    pub fn runs(&self) -> RunCollectionClient {
368        RunCollectionClient::new(self.ctx.http.clone(), &self.ctx.url(None), "runs")
369    }
370
371    /// Returns a client for a specific version of this Actor.
372    pub fn version(&self, version_number: &str) -> ActorVersionClient {
373        ActorVersionClient::new(self.ctx.http.clone(), &self.ctx.url(None), version_number)
374    }
375
376    /// Returns a client for this Actor's version collection.
377    pub fn versions(&self) -> ActorVersionCollectionClient {
378        ActorVersionCollectionClient::new(self.ctx.http.clone(), &self.ctx.url(None))
379    }
380
381    /// Returns a client for this Actor's webhook collection.
382    pub fn webhooks(&self) -> WebhookCollectionClient {
383        WebhookCollectionClient::with_base(self.ctx.http.clone(), &self.ctx.url(None))
384    }
385
386    /// The Actor's ID (or `username~name`) as provided.
387    pub fn id(&self) -> &str {
388        &self.id
389    }
390}