1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize)]
7pub struct CompositeRequest {
8 #[serde(rename = "allOrNone")]
9 pub all_or_none: bool,
10 #[serde(rename = "collateSubrequests")]
11 pub collate_subrequests: bool,
12 #[serde(rename = "compositeRequest")]
13 pub subrequests: Vec<CompositeSubrequest>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CompositeSubrequest {
19 pub method: String,
20 pub url: String,
21 #[serde(rename = "referenceId")]
22 pub reference_id: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub body: Option<serde_json::Value>,
25}
26
27#[derive(Debug, Clone, Deserialize)]
29pub struct CompositeResponse {
30 #[serde(rename = "compositeResponse")]
31 pub responses: Vec<CompositeSubresponse>,
32}
33
34#[derive(Debug, Clone, Deserialize)]
36pub struct CompositeSubresponse {
37 pub body: serde_json::Value,
38 #[serde(rename = "httpHeaders")]
39 pub http_headers: serde_json::Value,
40 #[serde(rename = "httpStatusCode")]
41 pub http_status_code: u16,
42 #[serde(rename = "referenceId")]
43 pub reference_id: String,
44}
45
46#[derive(Debug, Clone, Serialize)]
51pub struct CompositeBatchRequest {
52 #[serde(rename = "batchRequests")]
53 pub batch_requests: Vec<CompositeBatchSubrequest>,
54 #[serde(rename = "haltOnError")]
55 pub halt_on_error: bool,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct CompositeBatchSubrequest {
61 pub method: String,
62 pub url: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 #[serde(rename = "richInput")]
65 pub rich_input: Option<serde_json::Value>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 #[serde(rename = "binaryPartName")]
68 pub binary_part_name: Option<String>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 #[serde(rename = "binaryPartNameAlias")]
71 pub binary_part_name_alias: Option<String>,
72}
73
74#[derive(Debug, Clone, Deserialize)]
76pub struct CompositeBatchResponse {
77 #[serde(rename = "hasErrors")]
78 pub has_errors: bool,
79 pub results: Vec<CompositeBatchSubresponse>,
80}
81
82#[derive(Debug, Clone, Deserialize)]
84pub struct CompositeBatchSubresponse {
85 #[serde(rename = "statusCode")]
86 pub status_code: u16,
87 pub result: serde_json::Value,
88}
89
90#[derive(Debug, Clone, Serialize)]
95pub struct CompositeTreeRequest {
96 pub records: Vec<CompositeTreeRecord>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct CompositeTreeRecord {
102 pub attributes: CompositeTreeAttributes,
103 #[serde(rename = "referenceId")]
104 pub reference_id: String,
105 #[serde(flatten)]
106 pub fields: serde_json::Map<String, serde_json::Value>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CompositeTreeAttributes {
112 #[serde(rename = "type")]
113 pub sobject_type: String,
114}
115
116#[derive(Debug, Clone, Deserialize)]
118pub struct CompositeTreeResponse {
119 #[serde(rename = "hasErrors")]
120 pub has_errors: bool,
121 pub results: Vec<CompositeTreeResult>,
122}
123
124#[derive(Debug, Clone, Deserialize)]
126pub struct CompositeTreeResult {
127 #[serde(rename = "referenceId")]
128 pub reference_id: String,
129 pub id: Option<String>,
130 #[serde(default)]
131 pub errors: Vec<CompositeTreeError>,
132}
133
134#[derive(Debug, Clone, Deserialize)]
136pub struct CompositeTreeError {
137 #[serde(rename = "statusCode")]
138 pub status_code: String,
139 pub message: String,
140 pub fields: Vec<String>,
141}
142
143#[derive(Debug, Clone, Serialize)]
148pub struct CompositeGraphRequest {
149 pub graphs: Vec<GraphRequest>,
150}
151
152#[derive(Debug, Clone, Serialize)]
154#[serde(rename_all = "camelCase")]
155pub struct GraphRequest {
156 pub graph_id: String,
157 pub composite_request: Vec<CompositeSubrequest>,
158}
159
160#[derive(Debug, Clone, Deserialize)]
162pub struct CompositeGraphResponse {
163 pub graphs: Vec<GraphResponse>,
164}
165
166#[derive(Debug, Clone, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct GraphResponse {
170 pub graph_id: String,
171 pub graph_response: GraphResponseBody,
172 pub is_successful: bool,
173}
174
175#[derive(Debug, Clone, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct GraphResponseBody {
179 #[serde(rename = "compositeResponse")]
180 pub responses: Vec<CompositeSubresponse>,
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use serde_json::json;
187
188 #[test]
189 fn test_composite_request_serialization() {
190 let request = CompositeRequest {
191 all_or_none: true,
192 collate_subrequests: false,
193 subrequests: vec![
194 CompositeSubrequest {
195 method: "POST".to_string(),
196 url: "/services/data/v62.0/sobjects/Account".to_string(),
197 reference_id: "NewAccount".to_string(),
198 body: Some(json!({"Name": "Test Corp"})),
199 },
200 CompositeSubrequest {
201 method: "GET".to_string(),
202 url: "/services/data/v62.0/sobjects/Account/@{NewAccount.id}".to_string(),
203 reference_id: "GetAccount".to_string(),
204 body: None,
205 },
206 ],
207 };
208
209 let json = serde_json::to_value(&request).unwrap();
210 assert_eq!(json["allOrNone"], true);
211 assert_eq!(json["collateSubrequests"], false);
212 assert_eq!(json["compositeRequest"].as_array().unwrap().len(), 2);
213
214 let first = &json["compositeRequest"][0];
215 assert_eq!(first["method"], "POST");
216 assert_eq!(first["referenceId"], "NewAccount");
217 assert!(first["body"].is_object());
218
219 let second = &json["compositeRequest"][1];
221 assert_eq!(second["method"], "GET");
222 assert!(second.get("body").is_none());
223 }
224
225 #[test]
226 fn test_composite_response_deserialization() {
227 let json = json!({
228 "compositeResponse": [
229 {
230 "body": {"id": "001xx000003Dgb2AAC", "success": true, "errors": []},
231 "httpHeaders": {"Location": "/services/data/v62.0/sobjects/Account/001xx"},
232 "httpStatusCode": 201,
233 "referenceId": "NewAccount"
234 },
235 {
236 "body": {"Id": "001xx000003Dgb2AAC", "Name": "Test Corp"},
237 "httpHeaders": {},
238 "httpStatusCode": 200,
239 "referenceId": "GetAccount"
240 }
241 ]
242 });
243
244 let response: CompositeResponse = serde_json::from_value(json).unwrap();
245 assert_eq!(response.responses.len(), 2);
246 assert_eq!(response.responses[0].http_status_code, 201);
247 assert_eq!(response.responses[0].reference_id, "NewAccount");
248 assert_eq!(response.responses[1].http_status_code, 200);
249 }
250
251 #[test]
252 fn test_composite_batch_request_serialization() {
253 let request = CompositeBatchRequest {
254 batch_requests: vec![CompositeBatchSubrequest {
255 method: "GET".to_string(),
256 url: "/services/data/v62.0/sobjects/Account/001xx".to_string(),
257 rich_input: None,
258 binary_part_name: None,
259 binary_part_name_alias: None,
260 }],
261 halt_on_error: true,
262 };
263
264 let json = serde_json::to_value(&request).unwrap();
265 assert_eq!(json["haltOnError"], true);
266 assert_eq!(json["batchRequests"].as_array().unwrap().len(), 1);
267 assert!(json["batchRequests"][0].get("richInput").is_none());
269 }
270
271 #[test]
272 fn test_composite_batch_response_deserialization() {
273 let json = json!({
274 "hasErrors": true,
275 "results": [
276 {"statusCode": 200, "result": {"Id": "001xx", "Name": "Acme"}},
277 {"statusCode": 404, "result": [{"errorCode": "NOT_FOUND", "message": "not found"}]}
278 ]
279 });
280
281 let response: CompositeBatchResponse = serde_json::from_value(json).unwrap();
282 assert!(response.has_errors);
283 assert_eq!(response.results.len(), 2);
284 assert_eq!(response.results[0].status_code, 200);
285 assert_eq!(response.results[1].status_code, 404);
286 }
287
288 #[test]
289 fn test_composite_tree_request_serialization() {
290 let mut fields = serde_json::Map::new();
291 fields.insert("Name".to_string(), json!("Parent Account"));
292
293 let request = CompositeTreeRequest {
294 records: vec![CompositeTreeRecord {
295 attributes: CompositeTreeAttributes {
296 sobject_type: "Account".to_string(),
297 },
298 reference_id: "ref1".to_string(),
299 fields,
300 }],
301 };
302
303 let json = serde_json::to_value(&request).unwrap();
304 let record = &json["records"][0];
305 assert_eq!(record["attributes"]["type"], "Account");
306 assert_eq!(record["referenceId"], "ref1");
307 assert_eq!(record["Name"], "Parent Account");
308 }
309
310 #[test]
311 fn test_composite_tree_response_with_errors() {
312 let json = json!({
313 "hasErrors": true,
314 "results": [
315 {
316 "referenceId": "ref1",
317 "id": null,
318 "errors": [
319 {
320 "statusCode": "REQUIRED_FIELD_MISSING",
321 "message": "Required fields are missing: [Name]",
322 "fields": ["Name"]
323 }
324 ]
325 }
326 ]
327 });
328
329 let response: CompositeTreeResponse = serde_json::from_value(json).unwrap();
330 assert!(response.has_errors);
331 assert!(response.results[0].id.is_none());
332 assert_eq!(response.results[0].errors.len(), 1);
333 assert_eq!(
334 response.results[0].errors[0].status_code,
335 "REQUIRED_FIELD_MISSING"
336 );
337 assert_eq!(response.results[0].errors[0].fields, vec!["Name"]);
338 }
339
340 #[test]
341 fn test_composite_tree_response_success() {
342 let json = json!({
343 "hasErrors": false,
344 "results": [
345 {"referenceId": "ref1", "id": "001xx000003Dgb2AAC", "errors": []}
346 ]
347 });
348
349 let response: CompositeTreeResponse = serde_json::from_value(json).unwrap();
350 assert!(!response.has_errors);
351 assert_eq!(
352 response.results[0].id,
353 Some("001xx000003Dgb2AAC".to_string())
354 );
355 assert!(response.results[0].errors.is_empty());
356 }
357
358 #[test]
359 fn test_composite_graph_request_serialization() {
360 let request = CompositeGraphRequest {
361 graphs: vec![GraphRequest {
362 graph_id: "graph1".to_string(),
363 composite_request: vec![CompositeSubrequest {
364 method: "POST".to_string(),
365 url: "/services/data/v62.0/sobjects/Account".to_string(),
366 reference_id: "Account1".to_string(),
367 body: Some(json!({"Name": "Test"})),
368 }],
369 }],
370 };
371
372 let json = serde_json::to_value(&request).unwrap();
373 assert_eq!(json["graphs"][0]["graphId"], "graph1");
374 assert_eq!(json["graphs"][0]["compositeRequest"][0]["method"], "POST");
375 }
376
377 #[test]
378 fn test_composite_graph_response_deserialization() {
379 let json = json!({
380 "graphs": [
381 {
382 "graphId": "graph1",
383 "graphResponse": {
384 "compositeResponse": [
385 {
386 "body": {"id": "001xx1", "success": true, "errors": []},
387 "httpHeaders": {},
388 "httpStatusCode": 201,
389 "referenceId": "Account1"
390 }
391 ]
392 },
393 "isSuccessful": true
394 }
395 ]
396 });
397
398 let response: CompositeGraphResponse = serde_json::from_value(json).unwrap();
399 assert_eq!(response.graphs.len(), 1);
400 assert!(response.graphs[0].is_successful);
401 assert_eq!(response.graphs[0].graph_id, "graph1");
402 assert_eq!(response.graphs[0].graph_response.responses.len(), 1);
403 assert_eq!(
404 response.graphs[0].graph_response.responses[0].http_status_code,
405 201
406 );
407 }
408}