Skip to main content

contentstack_api_client_rs/client/delivery/
entries.rs

1use std::collections::HashMap;
2
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// A JSON query filter map - keys are field UIDs, values are match conditions.
8///
9/// Contentstack expects this serialized as a JSON string in the `query` param.
10///
11/// # Example
12///
13/// ```
14/// use contentstack_api_client_rs::Query;
15/// use serde_json::json;
16///
17/// let mut q = Query::new();
18/// q.insert("title".into(), json!("Hello World"));       // equals
19/// q.insert("price".into(), json!({ "$gt": 100 }));      // greater than
20/// q.insert("status".into(), json!({ "$in": ["a","b"] })); // in array
21/// ```
22pub type Query = HashMap<String, Value>;
23
24/// Query parameters for fetching multiple entries.
25pub struct GetManyParams<'a> {
26    /// JSON query filter - serialized internally, no need to stringify manually.
27    pub query: Option<&'a Query>,
28    /// Maximum number of entries to return.
29    pub limit: Option<u32>,
30    /// Number of entries to skip (for pagination).
31    pub skip: Option<u32>,
32    /// When `true`, includes the total entry count in the response.
33    pub include_count: Option<bool>,
34    /// Locale code to fetch entries for (e.g. `"en-us"`).
35    pub locale: Option<&'a str>,
36}
37
38#[derive(Serialize)]
39struct SerializedGetManyParams<'a> {
40    pub query: Option<String>,
41    pub limit: Option<u32>,
42    pub skip: Option<u32>,
43    pub include_count: Option<bool>,
44    pub locale: Option<&'a str>,
45}
46
47impl<'a> From<GetManyParams<'a>> for SerializedGetManyParams<'a> {
48    fn from(p: GetManyParams<'a>) -> Self {
49        Self {
50            query: p
51                .query
52                .map(|q| serde_json::to_string(q).expect("Failed to serialize query to JSON")),
53            limit: p.limit,
54            skip: p.skip,
55            include_count: p.include_count,
56            locale: p.locale,
57        }
58    }
59}
60
61/// Query parameters for fetching a single entry by UID.
62pub struct GetOneParams<'a> {
63    /// JSON query filter - serialized internally, no need to stringify manually.
64    pub query: Option<&'a Query>,
65    /// Locale code to fetch the entry for (e.g. `"en-us"`).
66    pub locale: Option<&'a str>,
67}
68
69#[derive(Serialize)]
70struct SerializedGetOneParams<'a> {
71    pub query: Option<String>,
72    pub locale: Option<&'a str>,
73}
74
75impl<'a> From<GetOneParams<'a>> for SerializedGetOneParams<'a> {
76    fn from(p: GetOneParams<'a>) -> Self {
77        Self {
78            query: p
79                .query
80                .map(|q| serde_json::to_string(q).expect("Failed to serialize query to JSON")),
81            locale: p.locale,
82        }
83    }
84}
85
86/// A Contentstack entry with system fields plus caller-defined custom fields.
87///
88/// System fields (`uid`, `title`, `locale`, etc.) are always present.
89/// `T` holds your content type's custom fields, deserialized from the same
90/// JSON object via `#[serde(flatten)]`.
91///
92/// # Example
93///
94/// ```no_run
95/// use serde::Deserialize;
96/// use contentstack_api_client_rs::Entry;
97///
98/// #[derive(Deserialize)]
99/// struct BlogPost {
100///     pub body: String,
101///     pub url: String,
102/// }
103///
104/// // entry.uid, entry.title - system fields
105/// // entry.fields.body     - your custom field
106/// ```
107#[derive(Debug, Deserialize)]
108pub struct Entry<T> {
109    pub uid: String,
110    pub title: String,
111    pub locale: String,
112    pub created_at: String,
113    pub updated_at: String,
114    pub created_by: String,
115    pub updated_by: String,
116    #[serde(rename = "_version")]
117    pub version: u32,
118    /// Caller's custom fields - flattened into the same JSON object.
119    #[serde(flatten)]
120    pub fields: T,
121}
122
123/// Response wrapper for a list of entries.
124///
125/// Contentstack returns `{ "entries": [...], "count": N }`.
126/// `count` is only present when `include_count: true` is set in params.
127#[derive(Debug, Deserialize)]
128pub struct EntriesResponse<T> {
129    pub entries: Vec<Entry<T>>,
130    pub count: Option<u32>,
131}
132
133/// Response wrapper for a single entry.
134///
135/// Contentstack returns `{ "entry": { ... } }`.
136#[derive(Debug, Deserialize)]
137pub struct EntryResponse<T> {
138    pub entry: Entry<T>,
139}
140
141/// Sub-client for the Entries endpoint.
142///
143/// Obtained via [`crate::Delivery::entries`] - never constructed directly.
144pub struct Entries<'a> {
145    pub client: &'a Client,
146}
147
148impl<'a> Entries<'a> {
149    /// Builds the entries URL for a given content type, with an optional entry UID.
150    fn build_url(content_type: &str, uid: Option<&str>) -> String {
151        match uid {
152            Some(u) => format!("/content_types/{}/entries/{}", content_type, u),
153            None => format!("/content_types/{}/entries", content_type),
154        }
155    }
156
157    /// Fetches multiple entries for a given content type.
158    ///
159    /// # Arguments
160    ///
161    /// * `content_type` - The content type UID (e.g. `"blog_post"`)
162    /// * `params` - Optional query parameters (filters, pagination, locale)
163    ///
164    /// # Example
165    ///
166    /// ```no_run
167    /// use serde::Deserialize;
168    /// use contentstack_api_client_rs::{Delivery, GetManyParams};
169    ///
170    /// #[derive(Deserialize)]
171    /// struct BlogPost { body: String }
172    ///
173    /// # async fn example() -> Result<(), reqwest::Error> {
174    /// let client = Delivery::new("api_key", "token", "production", None);
175    /// let response = client.entries()
176    ///     .get_many::<BlogPost>("blog_post", None)
177    ///     .await?;
178    ///
179    /// println!("Total: {}", response.entries.len());
180    /// # Ok(())
181    /// # }
182    /// ```
183    pub async fn get_many<T>(
184        &self,
185        content_type: &str,
186        params: Option<GetManyParams<'_>>,
187    ) -> Result<EntriesResponse<T>, reqwest::Error>
188    where
189        T: for<'de> Deserialize<'de>,
190    {
191        let request = self.client.get(Entries::build_url(content_type, None));
192
193        let request = if let Some(p) = params {
194            let serialized: SerializedGetManyParams = p.into();
195            request.query(&serialized)
196        } else {
197            request
198        };
199
200        request.send().await?.json::<EntriesResponse<T>>().await
201    }
202
203    /// Fetches a single entry by UID for a given content type.
204    ///
205    /// # Arguments
206    ///
207    /// * `content_type` - The content type UID (e.g. `"blog_post"`)
208    /// * `uid` - The entry UID to fetch
209    /// * `params` - Optional query parameters (locale, query filter)
210    ///
211    /// # Example
212    ///
213    /// ```no_run
214    /// use serde::Deserialize;
215    /// use contentstack_api_client_rs::{Delivery, GetOneParams};
216    ///
217    /// #[derive(Deserialize)]
218    /// struct BlogPost { body: String }
219    ///
220    /// # async fn example() -> Result<(), reqwest::Error> {
221    /// let client = Delivery::new("api_key", "token", "production", None);
222    /// let response = client.entries()
223    ///     .get_one::<BlogPost>("blog_post", "entry_uid_123", None)
224    ///     .await?;
225    ///
226    /// println!("Title: {}", response.entry.title);
227    /// # Ok(())
228    /// # }
229    /// ```
230    pub async fn get_one<T>(
231        &self,
232        content_type: &str,
233        uid: &str,
234        params: Option<GetOneParams<'_>>,
235    ) -> Result<EntryResponse<T>, reqwest::Error>
236    where
237        T: for<'de> Deserialize<'de>,
238    {
239        let request = self.client.get(Entries::build_url(content_type, Some(uid)));
240
241        let request = if let Some(p) = params {
242            let serialized: SerializedGetOneParams = p.into();
243            request.query(&serialized)
244        } else {
245            request
246        };
247
248        request.send().await?.json::<EntryResponse<T>>().await
249    }
250}