Skip to main content

apify_client/clients/
store_collection.rs

1//! Client for browsing the Apify Store (`/v2/store`).
2
3use crate::clients::base::{list_resource, ResourceContext};
4use crate::common::{PaginationList, QueryParams};
5use crate::error::ApifyClientResult;
6use crate::http_client::HttpClient;
7use crate::models::ActorStoreListItem;
8
9/// Options for searching the Apify Store.
10#[derive(Debug, Default, Clone)]
11pub struct StoreListOptions {
12    /// Number of items to skip.
13    pub offset: Option<i64>,
14    /// Maximum number of items to return per page.
15    pub limit: Option<i64>,
16    /// Full-text search query.
17    pub search: Option<String>,
18    /// Sort key (e.g. `popularity`, `newest`).
19    pub sort_by: Option<String>,
20    /// Filter by category.
21    pub category: Option<String>,
22    /// Filter by owner username.
23    pub username: Option<String>,
24    /// Filter by pricing model.
25    pub pricing_model: Option<String>,
26    /// Include Actors that the current user cannot run (e.g. needing a missing integration).
27    pub include_unrunnable_actors: Option<bool>,
28    /// Filter to Actors that allow agentic (x402/Skyfire) users.
29    pub allows_agentic_users: Option<bool>,
30    /// Response format requested from the API (e.g. `json`).
31    pub response_format: Option<String>,
32}
33
34/// Client for the Apify Store.
35#[derive(Debug, Clone)]
36pub struct StoreCollectionClient {
37    ctx: ResourceContext,
38}
39
40impl StoreCollectionClient {
41    pub(crate) fn new(http: HttpClient, base_url: &str) -> Self {
42        Self {
43            ctx: ResourceContext::collection(http, base_url, "store"),
44        }
45    }
46
47    /// Lists Actors from the Apify Store matching the given options.
48    ///
49    /// Awaiting once returns a single page; use [`StoreCollectionClient::iterate`] to lazily
50    /// iterate across pages.
51    pub async fn list(
52        &self,
53        options: StoreListOptions,
54    ) -> ApifyClientResult<PaginationList<ActorStoreListItem>> {
55        let params = self.build_params(&options);
56        list_resource(&self.ctx, None, &params).await
57    }
58
59    /// Lazily iterates all Store Actors matching `options`, fetching pages on demand.
60    ///
61    /// Returns a [`StoreActorIterator`] whose [`next`](StoreActorIterator::next) method
62    /// yields one Actor at a time, transparently fetching subsequent pages.
63    pub fn iterate(&self, options: StoreListOptions) -> StoreActorIterator {
64        StoreActorIterator {
65            client: self.clone(),
66            options,
67            buffer: std::collections::VecDeque::new(),
68            next_offset: 0,
69            total: None,
70            exhausted: false,
71        }
72    }
73
74    fn build_params(&self, options: &StoreListOptions) -> QueryParams {
75        let mut params = QueryParams::new();
76        params
77            .add_int("offset", options.offset)
78            .add_int("limit", options.limit)
79            .add_str("search", options.search.clone())
80            .add_str("sortBy", options.sort_by.clone())
81            .add_str("category", options.category.clone())
82            .add_str("username", options.username.clone())
83            .add_str("pricingModel", options.pricing_model.clone())
84            .add_bool("includeUnrunnableActors", options.include_unrunnable_actors)
85            .add_bool("allowsAgenticUsers", options.allows_agentic_users)
86            .add_str("responseFormat", options.response_format.clone());
87        params
88    }
89}
90
91/// A lazy, page-fetching iterator over Apify Store Actors.
92///
93/// Created by [`StoreCollectionClient::iterate`]. Each call to [`next`](Self::next)
94/// returns the next Actor, fetching another page from the API when the local buffer is
95/// exhausted, until all matching Actors have been yielded.
96pub struct StoreActorIterator {
97    client: StoreCollectionClient,
98    options: StoreListOptions,
99    buffer: std::collections::VecDeque<ActorStoreListItem>,
100    next_offset: i64,
101    total: Option<i64>,
102    exhausted: bool,
103}
104
105impl StoreActorIterator {
106    /// Returns the next Store Actor, or `None` when the listing is exhausted.
107    pub async fn next(&mut self) -> ApifyClientResult<Option<ActorStoreListItem>> {
108        if let Some(item) = self.buffer.pop_front() {
109            return Ok(Some(item));
110        }
111        if self.exhausted {
112            return Ok(None);
113        }
114
115        // Honour a caller-provided starting offset on the first fetch.
116        let start_offset = self.options.offset.unwrap_or(0) + self.next_offset;
117        let mut page_options = self.options.clone();
118        page_options.offset = Some(start_offset);
119
120        let page = self.client.list(page_options).await?;
121        if self.total.is_none() {
122            self.total = Some(page.total);
123        }
124        if page.items.is_empty() {
125            self.exhausted = true;
126            return Ok(None);
127        }
128
129        self.next_offset += page.items.len() as i64;
130        // Stop once we have walked past the total number of available items.
131        if let Some(total) = self.total {
132            if start_offset + page.items.len() as i64 >= total {
133                self.exhausted = true;
134            }
135        }
136        self.buffer.extend(page.items);
137        Ok(self.buffer.pop_front())
138    }
139}