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}