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