powerplatform_dataverse_service_client/
batch.rs

1use std::fmt::{Display, Write};
2
3use uuid::Uuid;
4
5use crate::{
6    client::VERSION,
7    entity::WriteEntity,
8    reference::Reference,
9    result::{IntoDataverseResult, Result},
10};
11
12/**
13Represents a batch of Microsoft Dataverse Requests
14
15Some restrictions apply for creating batches:
16- the batch size may not exceed 1000 calls
17- the batch execution time may not exceed 2 minutes
18
19the second restriction is especially tricky to handle because the execution time
20depends on the complexity of the entity in dataverse.
21So it is possible to create 300 records of an entity with low complexity
22but only 50 records of an entity with high complexity in that timeframe.
23
24Based on experience a batch size of 50 should be safe for all entities though
25
26# Examples
27```rust
28use uuid::Uuid;
29use serde::Serialize;
30use powerplatform_dataverse_service_client::{
31    batch::Batch,
32    client::Client,
33    entity::WriteEntity,
34    reference::{Reference, ReferenceStruct},
35    result::{Result, IntoDataverseResult}
36};
37
38async fn test() -> Result<()> {
39    let testy_contact = Contact {
40        contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
41        firstname: String::from("Testy"),
42        lastname: String::from("McTestface"),
43    };
44
45    let marianne_contact = Contact {
46        contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789abc").into_dataverse_result()?,
47        firstname: String::from("Marianne"),
48        lastname: String::from("McTestface"),
49    };
50
51    // this batch creates both contacts in one call
52    let mut batch = Batch::new("https://instance.crm.dynamics.com/");
53    batch.create(&testy_contact)?;
54    batch.create(&marianne_contact)?;
55
56    let client = Client::new_dummy(); // Please replace this with your preferred authentication method
57    client.execute(&batch).await?;
58    Ok(())
59}
60
61#[derive(Serialize)]
62struct Contact {
63    contactid: Uuid,
64    firstname: String,
65    lastname: String,
66}
67
68impl WriteEntity for Contact {}
69
70impl Reference for Contact {
71    fn get_reference(&self) -> ReferenceStruct {
72        ReferenceStruct::new(
73            "contacts",
74            self.contactid,
75        )
76    }
77}
78```
79*/
80pub struct Batch {
81    url: &'static str,
82    batch_id: Uuid,
83    dataset_id: Uuid,
84    payload: String,
85    next_content_id: u16,
86}
87
88impl Batch {
89    /// Creates a new empty batch with its own batch id and dataset id
90    pub fn new(url: &'static str) -> Self {
91        Self {
92            url,
93            batch_id: Uuid::new_v4(),
94            dataset_id: Uuid::new_v4(),
95            payload: String::new(),
96            next_content_id: 1,
97        }
98    }
99
100    /**
101    Clears the batch of its contents and generates a new batch id and
102    a new dataset id
103
104    Note that this can be used to prevent frequent allocations by reusing
105    the `Batch` instance and its buffer
106    */
107    pub fn reset(&mut self) {
108        self.batch_id = Uuid::new_v4();
109        self.dataset_id = Uuid::new_v4();
110        self.payload.clear();
111        self.next_content_id = 1;
112    }
113
114    /// returns the current batch id (This will change after a call to `reset()`)
115    pub fn get_batch_id(&self) -> Uuid {
116        self.batch_id
117    }
118
119    /// returns the current dataset id (This will change after a call to `reset()`)
120    pub fn get_dataset_id(&self) -> Uuid {
121        self.dataset_id
122    }
123
124    /// returns the current count of requests in this batch
125    pub fn get_count(&self) -> u16 {
126        self.next_content_id - 1
127    }
128
129    /**
130    Adds a Create Request for the given entity to this batch
131
132    Please note that this function can fail if a serde serialization error occurs
133
134    # Examples
135    ```rust
136    use uuid::Uuid;
137    use serde::Serialize;
138    use powerplatform_dataverse_service_client::{
139        batch::Batch,
140        client::Client,
141        entity::WriteEntity,
142        reference::{Reference, ReferenceStruct},
143        result::{Result, IntoDataverseResult}
144    };
145
146    async fn test() -> Result<()> {
147        let testy_contact = Contact {
148            contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
149            firstname: String::from("Testy"),
150            lastname: String::from("McTestface"),
151        };
152
153        let marianne_contact = Contact {
154            contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789abc").into_dataverse_result()?,
155            firstname: String::from("Marianne"),
156            lastname: String::from("McTestface"),
157        };
158
159        // this batch creates both contacts in one call
160        let mut batch = Batch::new("https://instance.crm.dynamics.com/");
161        batch.create(&testy_contact)?;
162        batch.create(&marianne_contact)?;
163
164        let client = Client::new_dummy(); // Please replace this with your preferred authentication method
165        client.execute(&batch).await?;
166        Ok(())
167    }
168
169    #[derive(Serialize)]
170    struct Contact {
171        contactid: Uuid,
172        firstname: String,
173        lastname: String,
174    }
175
176    impl WriteEntity for Contact {}
177
178    impl Reference for Contact {
179        fn get_reference(&self) -> ReferenceStruct {
180            ReferenceStruct::new(
181                "contacts",
182                self.contactid,
183            )
184        }
185    }
186    ```
187    */
188    pub fn create(&mut self, entity: &impl WriteEntity) -> Result<()> {
189        let reference = entity.get_reference();
190        let entity = serde_json::to_string(entity).into_dataverse_result()?;
191
192        write!(
193            self.payload,
194            "--changeset_{}\nContent-Type: application/http\nContent-Transfer-Encoding:binary\nContent-Id: {}\n\nPOST {}api/data/v{}/{} HTTP/1.1\nContent-Type: application/json;type=entry\n\n{}\n", 
195            self.dataset_id.as_simple(),
196            self.next_content_id,
197            self.url,
198            VERSION,
199            reference.entity_name,
200            entity
201        ).into_dataverse_result()?;
202
203        self.next_content_id += 1;
204        Ok(())
205    }
206
207    /**
208    Adds an Update Request for the given entity to this batch
209
210    Please note that this function can fail if a serde serialization error occurs
211
212    # Examples
213    ```rust
214    use uuid::Uuid;
215    use serde::Serialize;
216    use powerplatform_dataverse_service_client::{
217        batch::Batch,
218        client::Client,
219        entity::WriteEntity,
220        reference::{Reference, ReferenceStruct},
221        result::{Result, IntoDataverseResult}
222    };
223
224    async fn test() -> Result<()> {
225        let testy_contact = Contact {
226            contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
227            firstname: String::from("Testy"),
228            lastname: String::from("McTestface"),
229        };
230
231        let marianne_contact = Contact {
232            contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789abc").into_dataverse_result()?,
233            firstname: String::from("Marianne"),
234            lastname: String::from("McTestface"),
235        };
236
237        // this batch creates both contacts in one call
238        let mut batch = Batch::new("https://instance.crm.dynamics.com/");
239        batch.update(&testy_contact)?;
240        batch.update(&marianne_contact)?;
241
242        let client = Client::new_dummy(); // Please replace this with your preferred authentication method
243        client.execute(&batch).await?;
244        Ok(())
245    }
246
247    #[derive(Serialize)]
248    struct Contact {
249        contactid: Uuid,
250        firstname: String,
251        lastname: String,
252    }
253
254    impl WriteEntity for Contact {}
255
256    impl Reference for Contact {
257        fn get_reference(&self) -> ReferenceStruct {
258            ReferenceStruct::new(
259                "contacts",
260                self.contactid,
261            )
262        }
263    }
264    ```
265    */
266    pub fn update(&mut self, entity: &impl WriteEntity) -> Result<()> {
267        let reference = entity.get_reference();
268        let entity = serde_json::to_string(entity).into_dataverse_result()?;
269
270        write!(
271            self.payload,
272            "--changeset_{}\nContent-Type: application/http\nContent-Transfer-Encoding:binary\nContent-Id: {}\n\nPATCH {}api/data/v{}/{}({}) HTTP/1.1\nContent-Type: application/json;type=entry\nIf-Match: *\n\n{}\n", 
273            self.dataset_id.as_simple(),
274            self.next_content_id,
275            self.url,
276            VERSION,
277            reference.entity_name,
278            reference.entity_id,
279            entity
280        ).into_dataverse_result()?;
281
282        self.next_content_id += 1;
283        Ok(())
284    }
285
286    /**
287    Adds an Upsert Request for the given entity to this batch
288
289    Please note that this function can fail if a serde serialization error occurs
290
291    # Examples
292    ```rust
293    use uuid::Uuid;
294    use serde::Serialize;
295    use powerplatform_dataverse_service_client::{
296        batch::Batch,
297        client::Client,
298        entity::WriteEntity,
299        reference::{Reference, ReferenceStruct},
300        result::{Result, IntoDataverseResult}
301    };
302
303    async fn test() -> Result<()> {
304        let testy_contact = Contact {
305            contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
306            firstname: String::from("Testy"),
307            lastname: String::from("McTestface"),
308        };
309
310        let marianne_contact = Contact {
311            contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789abc").into_dataverse_result()?,
312            firstname: String::from("Marianne"),
313            lastname: String::from("McTestface"),
314        };
315
316        // this batch creates both contacts in one call
317        let mut batch = Batch::new("https://instance.crm.dynamics.com/");
318        batch.upsert(&testy_contact)?;
319        batch.upsert(&marianne_contact)?;
320
321        let client = Client::new_dummy(); // Please replace this with your preferred authentication method
322        client.execute(&batch).await?;
323        Ok(())
324    }
325
326    #[derive(Serialize)]
327    struct Contact {
328        contactid: Uuid,
329        firstname: String,
330        lastname: String,
331    }
332
333    impl WriteEntity for Contact {}
334
335    impl Reference for Contact {
336        fn get_reference(&self) -> ReferenceStruct {
337            ReferenceStruct::new(
338                "contacts",
339                self.contactid,
340            )
341        }
342    }
343    ```
344    */
345    pub fn upsert(&mut self, entity: &impl WriteEntity) -> Result<()> {
346        let reference = entity.get_reference();
347        let entity = serde_json::to_string(entity).into_dataverse_result()?;
348
349        write!(
350            self.payload,
351            "--changeset_{}\nContent-Type: application/http\nContent-Transfer-Encoding:binary\nContent-Id: {}\n\nPATCH {}api/data/v{}/{}({}) HTTP/1.1\nContent-Type: application/json;type=entry\n\n{}\n", 
352            self.dataset_id.as_simple(),
353            self.next_content_id,
354            self.url,
355            VERSION,
356            reference.entity_name,
357            reference.entity_id,
358            entity
359        ).into_dataverse_result()?;
360
361        self.next_content_id += 1;
362        Ok(())
363    }
364
365    /**
366    Adds a Delete Request for the given entity reference to this batch
367
368    Please note that this function can fail if a serde serialization error occurs
369
370    # Examples
371    ```rust
372    use uuid::Uuid;
373    use serde::Serialize;
374    use powerplatform_dataverse_service_client::{
375        batch::Batch,
376        client::Client,
377        entity::WriteEntity,
378        reference::{Reference, ReferenceStruct},
379        result::{Result, IntoDataverseResult}
380    };
381
382    async fn test() -> Result<()> {
383        let testy_contact = ReferenceStruct::new(
384            "contacts",
385            Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?
386        );
387
388        let marianne_contact = ReferenceStruct::new(
389            "contacts",
390            Uuid::parse_str("12345678-1234-1234-1234-123456789abc").into_dataverse_result()?
391        );
392
393        // this batch creates both contacts in one call
394        let mut batch = Batch::new("https://instance.crm.dynamics.com/");
395        batch.delete(&testy_contact)?;
396        batch.delete(&marianne_contact)?;
397
398        let client = Client::new_dummy(); // Please replace this with your preferred authentication method
399        client.execute(&batch).await?;
400        Ok(())
401    }
402    ```
403    */
404    pub fn delete(&mut self, entity: &impl Reference) -> Result<()> {
405        let reference = entity.get_reference();
406
407        write!(
408            self.payload,
409            "--changeset_{}\nContent-Type: application/http\nContent-Transfer-Encoding:binary\nContent-Id: {}\n\nDELETE {}api/data/v{}/{}({}) HTTP/1.1\n\n", 
410            self.dataset_id.as_simple(),
411            self.next_content_id,
412            self.url,
413            VERSION,
414            reference.entity_name,
415            reference.entity_id
416        ).into_dataverse_result()?;
417
418        self.next_content_id += 1;
419        Ok(())
420    }
421}
422
423impl Display for Batch {
424    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425        let batch_id = self.batch_id.as_simple();
426        let dataset_id = self.dataset_id.as_simple();
427
428        f.write_fmt(
429            format_args!(
430                "--batch_{}\nContent-Type: multipart/mixed; boundary=changeset_{}\n\n{}--changeset_{}--\n--batch_{}--",
431                batch_id,
432                dataset_id,
433                self.payload,
434                dataset_id,
435                batch_id,
436            )
437        )
438    }
439}