parse_rs/
object.rs

1// src/object.rs
2
3use crate::acl::ParseACL;
4use crate::client::ParseClient;
5use crate::types::date::ParseDate;
6use crate::ParseError;
7use serde::de::{DeserializeOwned, Deserializer};
8use serde::{Deserialize, Serialize};
9use serde_json::{json, Value};
10use std::collections::HashMap;
11
12// Helper function to deserialize a string into Option<ParseDate>
13pub fn deserialize_string_to_option_parse_date<'de, D>(
14    deserializer: D,
15) -> Result<Option<ParseDate>, D::Error>
16where
17    D: Deserializer<'de>,
18{
19    let s: Option<String> = Option::deserialize(deserializer)?;
20    Ok(s.map(ParseDate::new))
21}
22
23// Helper function to deserialize a string into ParseDate
24pub fn deserialize_string_to_parse_date<'de, D>(deserializer: D) -> Result<ParseDate, D::Error>
25where
26    D: Deserializer<'de>,
27{
28    let s: String = String::deserialize(deserializer)?;
29    Ok(ParseDate::new(s))
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ParseObject {
34    #[serde(skip_serializing_if = "Option::is_none", rename = "objectId")]
35    pub object_id: Option<String>,
36    #[serde(
37        deserialize_with = "deserialize_string_to_option_parse_date",
38        skip_serializing_if = "Option::is_none",
39        rename = "createdAt"
40    )]
41    pub created_at: Option<ParseDate>,
42    #[serde(
43        deserialize_with = "deserialize_string_to_option_parse_date",
44        skip_serializing_if = "Option::is_none",
45        rename = "updatedAt"
46    )]
47    pub updated_at: Option<ParseDate>,
48    #[serde(flatten)]
49    pub fields: HashMap<String, Value>,
50    #[serde(rename = "ACL", skip_serializing_if = "Option::is_none")]
51    pub acl: Option<ParseACL>,
52    #[serde(skip_serializing, default)]
53    // Should not be serialized, only used for context. Default if missing.
54    pub class_name: String,
55}
56
57impl ParseObject {
58    pub fn new(class_name: &str) -> Self {
59        ParseObject {
60            class_name: class_name.to_string(),
61            fields: HashMap::new(),
62            acl: None,
63            object_id: None,
64            created_at: None,
65            updated_at: None,
66        }
67    }
68
69    pub fn set<T: Serialize>(&mut self, field_name: &str, value: T) {
70        self.fields
71            .insert(field_name.to_string(), serde_json::to_value(value).unwrap());
72    }
73
74    pub fn get<T: DeserializeOwned>(&self, field_name: &str) -> Option<T> {
75        self.fields
76            .get(field_name)
77            .and_then(|v| serde_json::from_value(v.clone()).ok())
78    }
79
80    pub fn set_acl(&mut self, acl: ParseACL) {
81        self.acl = Some(acl);
82    }
83
84    pub fn increment(&mut self, field_name: &str, amount: i64) {
85        let op = json!({
86            "__op": "Increment",
87            "amount": amount
88        });
89        self.fields.insert(field_name.to_string(), op);
90    }
91
92    pub fn decrement(&mut self, field_name: &str, amount: i64) {
93        self.increment(field_name, -amount);
94    }
95
96    pub fn add_to_array<T: Serialize>(&mut self, field_name: &str, items: &[T]) {
97        let op = json!({
98            "__op": "Add",
99            "objects": items
100        });
101        self.fields.insert(field_name.to_string(), op);
102    }
103
104    pub fn add_unique_to_array<T: Serialize>(&mut self, field_name: &str, items: &[T]) {
105        let op = json!({
106            "__op": "AddUnique",
107            "objects": items
108        });
109        self.fields.insert(field_name.to_string(), op);
110    }
111
112    pub fn remove_from_array<T: Serialize>(&mut self, field_name: &str, items: &[T]) {
113        let op = json!({
114            "__op": "Remove",
115            "objects": items
116        });
117        self.fields.insert(field_name.to_string(), op);
118    }
119}
120
121#[derive(Deserialize, Debug, Clone)]
122#[serde(rename_all = "camelCase")]
123pub struct RetrievedParseObject {
124    pub object_id: String,
125    #[serde(deserialize_with = "deserialize_string_to_parse_date")]
126    pub created_at: ParseDate,
127    #[serde(deserialize_with = "deserialize_string_to_parse_date")]
128    pub updated_at: ParseDate,
129    #[serde(flatten)]
130    pub fields: HashMap<String, Value>,
131    #[serde(rename = "ACL")]
132    pub acl: Option<ParseACL>,
133}
134
135#[derive(Deserialize, Debug, Clone)]
136#[serde(rename_all = "camelCase")]
137pub struct CreateObjectResponse {
138    pub object_id: String,
139    #[serde(deserialize_with = "deserialize_string_to_parse_date")]
140    pub created_at: ParseDate,
141}
142
143#[derive(Deserialize, Debug, Clone)]
144#[serde(rename_all = "camelCase")]
145pub struct UpdateObjectResponse {
146    #[serde(deserialize_with = "deserialize_string_to_parse_date")]
147    pub updated_at: ParseDate,
148}
149
150impl ParseClient {
151    pub async fn create_object<T: Serialize + Send + Sync>(
152        &self,
153        class_name: &str,
154        data: &T,
155    ) -> Result<CreateObjectResponse, ParseError> {
156        if class_name.is_empty() {
157            return Err(ParseError::InvalidInput(
158                "Class name cannot be empty".to_string(),
159            ));
160        }
161        if !class_name
162            .chars()
163            .next()
164            .is_some_and(|c| c.is_alphabetic() || c == '_')
165        {
166            return Err(ParseError::InvalidInput(
167                "Invalid class name: must start with a letter or underscore.".to_string(),
168            ));
169        }
170        if !class_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
171            return Err(ParseError::InvalidInput(
172                "Invalid class name: can only contain letters, numbers, or underscores."
173                    .to_string(),
174            ));
175        }
176
177        let endpoint = format!("classes/{}", class_name);
178        match self.post(&endpoint, data).await {
179            Ok(res) => Ok(res),
180            Err(e) => Err(e),
181        }
182    }
183
184    pub async fn retrieve_object(
185        &self,
186        class_name: &str,
187        object_id: &str,
188    ) -> Result<RetrievedParseObject, ParseError> {
189        if class_name.is_empty() {
190            return Err(ParseError::InvalidInput(
191                "Class name cannot be empty".to_string(),
192            ));
193        }
194        if object_id.is_empty() {
195            return Err(ParseError::InvalidInput(
196                "Object ID cannot be empty".to_string(),
197            ));
198        }
199        if !class_name
200            .chars()
201            .next()
202            .is_some_and(|c| c.is_alphabetic() || c == '_')
203        {
204            return Err(ParseError::InvalidInput(
205                "Invalid class name: must start with a letter or underscore.".to_string(),
206            ));
207        }
208        if !class_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
209            return Err(ParseError::InvalidInput(
210                "Invalid class name: can only contain letters, numbers, or underscores."
211                    .to_string(),
212            ));
213        }
214
215        let endpoint = format!("classes/{}/{}", class_name, object_id);
216        self.get(&endpoint).await
217    }
218
219    pub async fn update_object<T: Serialize + Send + Sync>(
220        &self,
221        class_name: &str,
222        object_id: &str,
223        data: &T,
224    ) -> Result<UpdateObjectResponse, ParseError> {
225        if class_name.is_empty() {
226            return Err(ParseError::InvalidInput(
227                "Class name cannot be empty".to_string(),
228            ));
229        }
230        if object_id.is_empty() {
231            return Err(ParseError::InvalidInput(
232                "Object ID cannot be empty".to_string(),
233            ));
234        }
235        if !class_name
236            .chars()
237            .next()
238            .is_some_and(|c| c.is_alphabetic() || c == '_')
239        {
240            return Err(ParseError::InvalidInput(
241                "Invalid class name: must start with a letter or underscore.".to_string(),
242            ));
243        }
244        if !class_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
245            return Err(ParseError::InvalidInput(
246                "Invalid class name: can only contain letters, numbers, or underscores."
247                    .to_string(),
248            ));
249        }
250
251        let endpoint = format!("classes/{}/{}", class_name, object_id);
252        self.put(&endpoint, data).await
253    }
254
255    pub async fn delete_object(&self, class_name: &str, object_id: &str) -> Result<(), ParseError> {
256        if class_name.is_empty() {
257            return Err(ParseError::InvalidInput(
258                "Class name cannot be empty".to_string(),
259            ));
260        }
261        if object_id.is_empty() {
262            return Err(ParseError::InvalidInput(
263                "Object ID cannot be empty".to_string(),
264            ));
265        }
266        if !class_name
267            .chars()
268            .next()
269            .is_some_and(|c| c.is_alphabetic() || c == '_')
270        {
271            return Err(ParseError::InvalidInput(
272                "Invalid class name: must start with a letter or underscore.".to_string(),
273            ));
274        }
275        if !class_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
276            return Err(ParseError::InvalidInput(
277                "Invalid class name: can only contain letters, numbers, or underscores."
278                    .to_string(),
279            ));
280        }
281
282        let endpoint = format!("classes/{}/{}", class_name, object_id);
283        let response_value: Value = self.delete::<Value>(&endpoint).await?;
284
285        if response_value.is_object()
286            && response_value.as_object().is_some_and(|obj| obj.is_empty())
287        {
288            Ok(())
289        } else {
290            Err(ParseError::UnexpectedResponse(format!(
291                "Expected empty JSON object {{}} for delete, got: {:?}",
292                response_value
293            )))
294        }
295    }
296}