Skip to main content

busbar_sf_rest/
composite.rs

1//! Composite API operations.
2
3use serde::{Deserialize, Serialize};
4
5/// A composite request containing multiple subrequests.
6#[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/// A single subrequest within a composite request.
17#[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/// Response from a composite request.
28#[derive(Debug, Clone, Deserialize)]
29pub struct CompositeResponse {
30    #[serde(rename = "compositeResponse")]
31    pub responses: Vec<CompositeSubresponse>,
32}
33
34/// Response from a single subrequest.
35#[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/// A composite batch request containing multiple independent subrequests.
47///
48/// Unlike the standard composite request, batch subrequests are executed independently
49/// and cannot reference each other's results. Available since API v34.0.
50#[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/// A single subrequest within a composite batch request.
59#[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/// Response from a composite batch request.
75#[derive(Debug, Clone, Deserialize)]
76pub struct CompositeBatchResponse {
77    #[serde(rename = "hasErrors")]
78    pub has_errors: bool,
79    pub results: Vec<CompositeBatchSubresponse>,
80}
81
82/// Response from a single batch subrequest.
83#[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/// A composite tree request for creating record hierarchies.
91///
92/// Allows creation of parent records with nested child records in a single request.
93/// Available since API v42.0.
94#[derive(Debug, Clone, Serialize)]
95pub struct CompositeTreeRequest {
96    pub records: Vec<CompositeTreeRecord>,
97}
98
99/// A record in a composite tree request with optional nested child records.
100#[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/// Attributes for a record in a composite tree request.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CompositeTreeAttributes {
112    #[serde(rename = "type")]
113    pub sobject_type: String,
114}
115
116/// Response from a composite tree request.
117#[derive(Debug, Clone, Deserialize)]
118pub struct CompositeTreeResponse {
119    #[serde(rename = "hasErrors")]
120    pub has_errors: bool,
121    pub results: Vec<CompositeTreeResult>,
122}
123
124/// Result of a single record creation in a composite tree request.
125#[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/// Error details for a failed record creation in a composite tree request.
135#[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/// A composite graph request for executing multiple dependent operations.
144///
145/// Allows multiple independent graphs that each contain composite subrequests.
146/// Available since API v50.0.
147#[derive(Debug, Clone, Serialize)]
148pub struct CompositeGraphRequest {
149    pub graphs: Vec<GraphRequest>,
150}
151
152/// A single graph within a composite graph request.
153#[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/// Response from a composite graph request.
161#[derive(Debug, Clone, Deserialize)]
162pub struct CompositeGraphResponse {
163    pub graphs: Vec<GraphResponse>,
164}
165
166/// Response from a single graph.
167#[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/// Body of a graph response containing the composite responses.
176#[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        // GET subrequest should omit null body
220        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        // Optional fields should be omitted
268        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}