powerplatform_dataverse_service_client/client.rs
1/*!
2module for creating clients with various authentication methods
3
4Each client has the type `Client<A: Authenticate>`.
5You can create a client with the functions provided by this module.
6
7# Examples
8```rust
9use powerplatform_dataverse_service_client::client::Client;
10
11let client_id = "<clientid>";
12let client_secret = "<clientsecret>";
13
14let client = Client::with_client_secret_auth(
15 "https://instance.crm.dynamics.com/",
16 "12345678-1234-1234-1234-123456789012",
17 client_id,
18 client_secret,
19);
20```
21*/
22
23use std::future::Future;
24use std::{borrow::Cow, fmt::Display};
25use std::time::Duration;
26
27use lazy_static::lazy_static;
28use regex::Regex;
29use reqwest::{RequestBuilder, Response, Method};
30use serde::Deserialize;
31use uuid::Uuid;
32
33use crate::action::MergeRequest;
34use crate::{
35 auth::{client_secret::ClientSecretAuth, Authenticate, no_auth::NoAuth},
36 batch::Batch,
37 entity::{ReadEntity, WriteEntity},
38 error::DataverseError,
39 query::Query,
40 reference::Reference,
41 result::{IntoDataverseResult, Result},
42};
43
44lazy_static! {
45 static ref UUID_REGEX: Regex =
46 Regex::new("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
47 .unwrap();
48}
49
50/// Microsoft Dataverse Web-API Version this client uses
51pub static VERSION: &str = "9.2";
52/**
53A client capable of connecting to a dataverse environment
54
55A client should be created once and then reused to take advantage of its
56connection-pooling.
57
58# Examples
59```rust
60use powerplatform_dataverse_service_client::client::Client;
61
62let client_id = "<clientid>";
63let client_secret ="<clientsecret>";
64
65let client = Client::with_client_secret_auth(
66 "https://instance.crm.dynamics.com/",
67 "12345678-1234-1234-1234-123456789012",
68 client_id,
69 client_secret,
70);
71```
72*/
73pub struct Client<'url, A: Authenticate> {
74 pub url: Cow<'url, str>,
75 backend: reqwest::Client,
76 auth: A,
77}
78
79impl<'url> Client<'url, ClientSecretAuth> {
80 /**
81 Creates a dataverse client that uses client/secret authentication
82
83 Please note that this function will not fail right away even when the
84 provided credentials are invalid. This is because the authentication
85 is handled lazily and a token is only acquired on the first call or
86 when an acquired token is no longer valid and needs to be refreshed
87
88 # Examples
89 ```rust
90 use powerplatform_dataverse_service_client::client::Client;
91
92 let client_id = "<clientid>";
93 let client_secret = "<clientsecret>";
94
95 let client = Client::with_client_secret_auth(
96 "https://instance.crm.dynamics.com/",
97 "12345678-1234-1234-1234-123456789012",
98 client_id,
99 client_secret,
100 );
101 ```
102 */
103 pub fn with_client_secret_auth(
104 url: impl Into<Cow<'url, str>>,
105 tenant_id: &str,
106 client_id: impl Into<String>,
107 client_secret: impl Into<String>,
108 ) -> Self {
109 let url = url.into();
110 let client_id = client_id.into();
111 let client_secret = client_secret.into();
112 let client = reqwest::Client::builder()
113 .https_only(true)
114 .connect_timeout(Duration::from_secs(120))
115 .timeout(Duration::from_secs(120))
116 .build()
117 .unwrap();
118
119 let auth = ClientSecretAuth::new(
120 client.clone(),
121 format!(
122 "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
123 tenant_id
124 ),
125 format!("{}.default", url),
126 client_id,
127 client_secret,
128 );
129
130 Client::new(url, client, auth)
131 }
132}
133
134impl<'url> Client<'url, NoAuth> {
135 /**
136 Creates a dummy Client that will return errors every time its functions are used
137
138 This is only really useful in unit-testing and doc-testing scenarios where you
139 want to prevent a bunch of erronous auth-calls each time a test is run
140 */
141 pub fn new_dummy() -> Self {
142 let client = reqwest::Client::builder()
143 .https_only(true)
144 .connect_timeout(Duration::from_secs(120))
145 .timeout(Duration::from_secs(120))
146 .build()
147 .unwrap();
148
149 let auth = NoAuth {};
150 Client::new("", client, auth)
151 }
152}
153
154impl<'url, A: Authenticate> Client<'url, A> {
155 /**
156 Creates a dataverse client with a custom authentication handler and backend
157
158 This function may not panic so the custom authentication should follow these
159 rules:
160 - tokens should be acquired lazily
161 - tokens should be cached and reused where possible
162 - each call to the `get_valid_token()` function should give a token that is valid
163 for at least the next 2 minutes
164
165 # Examples
166 ```rust
167 use core::time::Duration;
168 use powerplatform_dataverse_service_client::auth::client_secret::ClientSecretAuth;
169 use powerplatform_dataverse_service_client::client::Client;
170 use powerplatform_dataverse_service_client::result::{IntoDataverseResult, Result};
171
172 # fn main() -> Result<()> {
173 let tenant_id = "12345678-1234-1234-1234-123456789012";
174 let client_id = String::from("<some client id>");
175 let client_secret = String::from("<some client secret>");
176 let url = "https://instance.crm.dynamics.crm/";
177
178 let client = reqwest::Client::builder()
179 .https_only(true)
180 .connect_timeout(Duration::from_secs(120))
181 .timeout(Duration::from_secs(120))
182 .build().into_dataverse_result()?;
183
184 let auth = ClientSecretAuth::new(
185 client.clone(),
186 format!(
187 "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
188 tenant_id
189 ),
190 format!("{}.default", url),
191 client_id,
192 client_secret,
193 );
194
195 let client = Client::new(url, client, auth);
196 # Ok(())
197 # }
198 ```
199 */
200 pub fn new(url: impl Into<Cow<'url, str>>, backend: reqwest::Client, auth: A) -> Self {
201 let url = url.into();
202 Self { url, backend, auth }
203 }
204
205 /**
206 Writes the given entity into the current dataverse instance and returns its generated Uuid
207
208 This may fail for any of these reasons
209 - An authentication failure
210 - A serde serialization error
211 - Any http client or server error
212 - there is already a record with this Uuid in the table
213
214 # Examples
215 ```rust
216 use uuid::Uuid;
217 use serde::Serialize;
218 use powerplatform_dataverse_service_client::client::Client;
219 use powerplatform_dataverse_service_client::entity::WriteEntity;
220 use powerplatform_dataverse_service_client::reference::{Reference, ReferenceStruct};
221 use powerplatform_dataverse_service_client::result::{IntoDataverseResult, Result};
222
223 async fn test() -> Result<Uuid> {
224 let contact = Contact {
225 contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
226 firstname: String::from("Testy"),
227 lastname: String::from("McTestface"),
228 };
229
230 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
231 client.create(&contact).await
232 }
233
234 #[derive(Serialize)]
235 struct Contact {
236 contactid: Uuid,
237 firstname: String,
238 lastname: String,
239 }
240
241 impl WriteEntity for Contact {}
242
243 impl Reference for Contact {
244 fn get_reference(&self) -> ReferenceStruct {
245 ReferenceStruct::new(
246 "contacts",
247 self.contactid,
248 )
249 }
250 }
251 ```
252 */
253 pub async fn create(&self, entity: &impl WriteEntity) -> Result<Uuid> {
254 let reference = entity.get_reference();
255 let url_path = self.build_simple_url(reference.entity_name);
256
257 async fn handle_response(response: Response) -> Result<Uuid> {
258 if response.status().is_client_error() || response.status().is_server_error() {
259 let error_message = response
260 .text()
261 .await
262 .unwrap_or_else(|_| String::from("no error details provided from server"));
263 return Err(DataverseError::new(error_message));
264 }
265
266 let header_value = response
267 .headers()
268 .get("OData-EntityId")
269 .ok_or_else(|| DataverseError::new("Dataverse provided no Uuid".to_string()))?;
270
271 let uuid_segment = UUID_REGEX
272 .find(header_value.to_str().unwrap_or(""))
273 .ok_or_else(|| DataverseError::new("Dataverse provided no Uuid".to_string()))?;
274
275 Uuid::parse_str(uuid_segment.as_str()).into_dataverse_result()
276 }
277
278 self.request(
279 Method::POST,
280 &url_path,
281 move |request| {
282 Ok(request
283 .header("Content-Type", "application/json")
284 .body(serde_json::to_vec(entity).into_dataverse_result()?)
285 )
286 },
287 handle_response
288 ).await
289 }
290
291 /**
292 Updates the attributes of the gven entity in the current dataverse instance
293
294 Please note that only those attributes are updated that are present in the
295 serialization payload. Other attributes are untouched
296
297 This may fail for any of these reasons
298 - An authentication failure
299 - A serde serialization error
300 - Any http client or server error
301 - there is no record with this Uuid in the table
302
303 # Examples
304 ```rust
305 use uuid::Uuid;
306 use serde::Serialize;
307 use powerplatform_dataverse_service_client::client::Client;
308 use powerplatform_dataverse_service_client::entity::WriteEntity;
309 use powerplatform_dataverse_service_client::reference::{Reference, ReferenceStruct};
310 use powerplatform_dataverse_service_client::result::{IntoDataverseResult, Result};
311
312 async fn test() -> Result<()> {
313 let contact = Contact {
314 contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
315 firstname: String::from("Testy"),
316 lastname: String::from("McTestface"),
317 };
318
319 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
320 client.update(&contact).await
321 }
322
323 #[derive(Serialize)]
324 struct Contact {
325 contactid: Uuid,
326 firstname: String,
327 lastname: String,
328 }
329
330 impl WriteEntity for Contact {}
331
332 impl Reference for Contact {
333 fn get_reference(&self) -> ReferenceStruct {
334 ReferenceStruct::new(
335 "contacts",
336 self.contactid,
337 )
338 }
339 }
340 ```
341 */
342 pub async fn update(&self, entity: &impl WriteEntity) -> Result<()> {
343 let reference = entity.get_reference();
344 let url_path = self.build_targeted_url(reference.entity_name, reference.entity_id);
345
346 self.request(
347 Method::PATCH,
348 &url_path,
349 move |request| {
350 Ok(request
351 .header("Content-Type", "application/json")
352 .header("If-Match", "*")
353 .body(serde_json::to_vec(entity).into_dataverse_result()?)
354 )
355 },
356 handle_empty_response
357 ).await
358 }
359
360 /**
361 Updates or creates the given entity in the current dataverse instance
362
363 Please note that only those attributes are updated that are present in the
364 serialization payload. Other attributes are untouched
365
366 This may fail for any of these reasons
367 - An authentication failure
368 - A serde serialization error
369 - Any http client or server error
370
371 # Examples
372 ```rust
373 use uuid::Uuid;
374 use serde::Serialize;
375 use powerplatform_dataverse_service_client::client::Client;
376 use powerplatform_dataverse_service_client::entity::WriteEntity;
377 use powerplatform_dataverse_service_client::reference::{Reference, ReferenceStruct};
378 use powerplatform_dataverse_service_client::result::{IntoDataverseResult, Result};
379
380 async fn test() -> Result<()> {
381 let contact = Contact {
382 contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
383 firstname: String::from("Testy"),
384 lastname: String::from("McTestface"),
385 };
386
387 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
388 client.upsert(&contact).await
389 }
390
391 #[derive(Serialize)]
392 struct Contact {
393 contactid: Uuid,
394 firstname: String,
395 lastname: String,
396 }
397
398 impl WriteEntity for Contact {}
399
400 impl Reference for Contact {
401 fn get_reference(&self) -> ReferenceStruct {
402 ReferenceStruct::new(
403 "contacts",
404 self.contactid,
405 )
406 }
407 }
408 ```
409 */
410 pub async fn upsert(&self, entity: &impl WriteEntity) -> Result<()> {
411 let reference = entity.get_reference();
412 let url_path = self.build_targeted_url(reference.entity_name, reference.entity_id);
413
414 self.request(
415 Method::PATCH,
416 &url_path,
417 move |request| {
418 Ok(request
419 .header("Content-Type", "application/json")
420 .body(serde_json::to_vec(entity).into_dataverse_result()?)
421 )
422 },
423 handle_empty_response
424 ).await
425 }
426
427 /**
428 Deletes the entity record this reference points to
429
430 Please note that each structs that implements `WriteEntity` also implements
431 `Reference` so you can use it as input here, but there is a sensible default implementation
432 called `ReferenceStruct` for those cases where you only have access to the raw
433 reference data
434
435 This may fail for any of these reasons
436 - An authentication failure
437 - Any http client or server error
438 - The referenced entity record doesn't exist
439
440 # Examples
441 ```rust
442 use uuid::Uuid;
443 use powerplatform_dataverse_service_client::client::Client;
444 use powerplatform_dataverse_service_client::reference::ReferenceStruct;
445 use powerplatform_dataverse_service_client::result::{IntoDataverseResult, Result};
446
447 # async fn test() -> Result<()> {
448 let reference = ReferenceStruct::new(
449 "contacts",
450 Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?
451 );
452
453 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
454 client.delete(&reference).await?;
455 # Ok(())
456 # }
457 ```
458 */
459 pub async fn delete(&self, reference: &impl Reference) -> Result<()> {
460 let reference = reference.get_reference();
461 let url_path = self.build_targeted_url(reference.entity_name, reference.entity_id);
462
463 self.request(
464 Method::DELETE,
465 &url_path,
466 move |request| Ok(request),
467 handle_empty_response
468 ).await
469 }
470
471 /**
472 retrieves the entity record that the reference points to from dataverse
473
474 This function uses the implementation of the `Select` trait to only retrieve
475 those attributes relevant to the struct defined. It is an Anti-Pattern to
476 retrieve all attributes when they are not needed, so this library does not
477 give the option to do that
478
479 This may fail for any of these reasons
480 - An authentication failure
481 - A serde deserialization error
482 - Any http client or server error
483 - The entity record referenced doesn't exist
484
485 # Examples
486 ```rust
487 use serde::Deserialize;
488 use uuid::Uuid;
489 use powerplatform_dataverse_service_client::{
490 client::Client,
491 entity::ReadEntity,
492 reference::ReferenceStruct,
493 result::{IntoDataverseResult, Result},
494 select::Select
495 };
496
497 async fn test() -> Result<()> {
498 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
499 let contact: Contact = client
500 .retrieve(
501 &ReferenceStruct::new(
502 "contacts",
503 Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?
504 )
505 )
506 .await?;
507 Ok(())
508 }
509
510 #[derive(Deserialize)]
511 struct Contact {
512 contactid: Uuid,
513 firstname: String,
514 lastname: String,
515 }
516
517 impl ReadEntity for Contact {}
518
519 impl Select for Contact {
520 fn get_columns() -> &'static [&'static str] {
521 &["contactid", "firstname", "lastname"]
522 }
523 }
524 ```
525 */
526 pub async fn retrieve<E: ReadEntity>(&self, reference: &impl Reference) -> Result<E> {
527 let reference = reference.get_reference();
528 let columns = E::get_columns();
529 let url_path = self.build_retrieve_url(reference.entity_name, reference.entity_id, columns);
530
531 async fn handle_response<E: ReadEntity>(response: Response) -> Result<E> {
532 if response.status().is_client_error() || response.status().is_server_error() {
533 let error_message = response
534 .text()
535 .await
536 .unwrap_or_else(|_| String::from("no error details provided from server"));
537 return Err(DataverseError::new(error_message));
538 }
539
540 let content = response.bytes().await.into_dataverse_result()?;
541 serde_json::from_slice(content.as_ref()).into_dataverse_result()
542 }
543
544 self.request(
545 Method::GET,
546 &url_path,
547 move |request| Ok(request),
548 handle_response
549 ).await
550 }
551
552 /**
553 Executes the query and retrieves the entities from dataverse
554
555 This function uses the implementation of the `Select` trait to only retrieve
556 those attributes relevant to the struct defined. It is an Anti-Pattern to
557 retrieve all attributes when they are not needed, so this library does not
558 give the option to do that
559
560 Please note that if you don't specify a limit then the client will try to retrieve
561 up to 5000 records. Further records can then be retrieved with the `retrieve_next_page()`
562 function
563
564 This may fail for any of these reasons
565 - An authentication failure
566 - A serde deserialization error
567 - Any http client or server error
568
569 # Examples
570 ```rust
571 use uuid::Uuid;
572 use serde::Deserialize;
573 use powerplatform_dataverse_service_client::{
574 client::{Client, Page},
575 entity::ReadEntity,
576 reference::ReferenceStruct,
577 result::{IntoDataverseResult, Result},
578 select::Select,
579 query::Query
580 };
581
582 async fn test() -> Result<()> {
583 // this query retrieves the first 3 contacts
584 let query = Query::new("contacts").limit(3);
585 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
586 let contacts: Page<Contact> = client.retrieve_multiple(&query).await?;
587 Ok(())
588 }
589
590 #[derive(Deserialize)]
591 struct Contact {
592 contactid: Uuid,
593 firstname: String,
594 lastname: String,
595 }
596
597 impl ReadEntity for Contact {}
598
599 impl Select for Contact {
600 fn get_columns() -> &'static [&'static str] {
601 &["contactid", "firstname", "lastname"]
602 }
603 }
604 ```
605 */
606 pub async fn retrieve_multiple<E: ReadEntity>(&self, query: &Query) -> Result<Page<E>> {
607 let columns = E::get_columns();
608 let url_path = self.build_query_url(query.logical_name, columns, query);
609
610 async fn handle_response<E: ReadEntity>(response: Response) -> Result<Page<E>> {
611 if response.status().is_client_error() || response.status().is_server_error() {
612 let error_message = response
613 .text()
614 .await
615 .unwrap_or_else(|_| String::from("no error details provided from server"));
616 return Err(DataverseError::new(error_message));
617 }
618
619 let content = response.bytes().await.into_dataverse_result()?;
620 let result = serde_json::from_slice(content.as_ref()).into_dataverse_result()?;
621
622 match result {
623 RetrieveMultipleResult {entities, next_link} => {
624 Ok(Page::new(entities, next_link))
625 }
626 }
627 }
628
629 self.request(
630 Method::GET,
631 &url_path,
632 move |request| Ok(request),
633 handle_response
634 ).await
635 }
636
637 /**
638 Continues a previous query by fetching the next records after a `Page`
639
640 You can check with `is_incomplete()` if there are further records available to a query
641
642 This may fail for any of these reasons
643 - An authentication failure
644 - A serde deserialization error
645 - Any http client or server error
646 - The query already finished with the last page
647
648 # Examples
649 ```rust
650 use uuid::Uuid;
651 use serde::Deserialize;
652 use powerplatform_dataverse_service_client::{
653 client::{Client, Page},
654 entity::ReadEntity,
655 reference::ReferenceStruct,
656 result::{IntoDataverseResult, Result},
657 select::Select,
658 query::Query
659 };
660
661 async fn test() -> Result<()> {
662 let query = Query::new("contacts");
663 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
664 let contact_page1: Page<Contact> = client.retrieve_multiple(&query).await?;
665
666 if contact_page1.is_incomplete() {
667 let contact_page2 = client.retrieve_next_page(&contact_page1).await?;
668 }
669
670 Ok(())
671 }
672
673 #[derive(Deserialize)]
674 struct Contact {
675 contactid: Uuid,
676 firstname: String,
677 lastname: String,
678 }
679
680 impl ReadEntity for Contact {}
681
682 impl Select for Contact {
683 fn get_columns() -> &'static [&'static str] {
684 &["contactid", "firstname", "lastname"]
685 }
686 }
687 ```
688 */
689 pub async fn retrieve_next_page<E: ReadEntity>(&self, previous_page: &Page<E>) -> Result<Page<E>> {
690 if previous_page.next_link.is_none() {
691 return Err(DataverseError::new(String::from("There is no next page to retrieve")))
692 }
693
694 async fn handle_response<E: ReadEntity>(response: Response) -> Result<Page<E>> {
695 if response.status().is_client_error() || response.status().is_server_error() {
696 let error_message = response
697 .text()
698 .await
699 .unwrap_or_else(|_| String::from("no error details provided from server"));
700 return Err(DataverseError::new(error_message));
701 }
702
703 let content = response.bytes().await.into_dataverse_result()?;
704 let result = serde_json::from_slice(content.as_ref()).into_dataverse_result()?;
705
706 match result {
707 RetrieveMultipleResult {entities, next_link} => {
708 Ok(Page::new( entities, next_link))
709 }
710 }
711 }
712
713 self.request(
714 Method::GET,
715 previous_page.next_link.as_ref().unwrap(),
716 move |request| Ok(request),
717 handle_response
718 ).await
719 }
720
721 /**
722 executes the batch against the dataverse environment
723
724 This function will fail if:
725 - the batch size exceeds 1000 calls
726 - the batch execution time exceeds 2 minutes
727
728 the second restriction is especially tricky to handle because the execution time
729 depends on the complexity of the entity in dataverse.
730 So it is possible to create 300 records of an entity with low complexity
731 but only 50 records of an entity with high complexity in that timeframe.
732
733 Based on experience a batch size of 50 should be safe for all entities though
734
735 # Examples
736 ```rust
737 use uuid::Uuid;
738 use serde::Serialize;
739 use powerplatform_dataverse_service_client::{
740 batch::Batch,
741 client::Client,
742 entity::WriteEntity,
743 reference::{Reference, ReferenceStruct},
744 result::{IntoDataverseResult, Result}
745 };
746
747 async fn test() -> Result<()> {
748 let testy_contact = Contact {
749 contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?,
750 firstname: String::from("Testy"),
751 lastname: String::from("McTestface"),
752 };
753
754 let marianne_contact = Contact {
755 contactid: Uuid::parse_str("12345678-1234-1234-1234-123456789abc").into_dataverse_result()?,
756 firstname: String::from("Marianne"),
757 lastname: String::from("McTestface"),
758 };
759
760 // this batch creates both contacts in one call
761 let mut batch = Batch::new("https://instance.crm.dynamics.com/");
762 batch.create(&testy_contact)?;
763 batch.create(&marianne_contact)?;
764 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
765 client.execute(&batch).await?;
766 Ok(())
767 }
768
769 #[derive(Serialize)]
770 struct Contact {
771 contactid: Uuid,
772 firstname: String,
773 lastname: String,
774 }
775
776 impl WriteEntity for Contact {}
777
778 impl Reference for Contact {
779 fn get_reference(&self) -> ReferenceStruct {
780 ReferenceStruct::new(
781 "contacts",
782 self.contactid,
783 )
784 }
785 }
786 ```
787 */
788 pub async fn execute(&self, batch: &Batch) -> Result<()> {
789 let url_path = self.build_simple_url("$batch");
790
791 self.request(
792 Method::POST,
793 &url_path,
794 move |request| {
795 Ok(request
796 .header("Content-Type", format!("multipart/mixed; boundary=batch_{}", batch.get_batch_id()))
797 .body(batch.to_string())
798 )
799 },
800 handle_empty_response
801 ).await
802 }
803
804 /**
805 Tries to merge two entities with and deactivates the subordinate after the process
806
807 This method is only supported for the following entities:
808 - account
809 - contact
810 - lead
811 - incident
812
813 # Examples
814 ```rust
815 use uuid::Uuid;
816 use powerplatform_dataverse_service_client::client::Client;
817 use powerplatform_dataverse_service_client::result::{IntoDataverseResult, Result};
818
819 async fn test() -> Result<()> {
820 let target_id = Uuid::parse_str("12345678-1234-1234-1234-123456789012").into_dataverse_result()?;
821 let subordinate_id = Uuid::parse_str("12345687-1234-1234-1234-123456879012").into_dataverse_result()?;
822
823 let client = Client::new_dummy(); // Please replace this with your preferred authentication method
824 client.merge("account", target_id, subordinate_id).await
825 }
826 ```
827 */
828 pub async fn merge(&self, entity_name: impl Display, target: Uuid, subordinate: Uuid) -> Result<()> {
829 let url_path = self.build_simple_url("Merge");
830
831 self.request(
832 Method::POST,
833 &url_path,
834 move |request| {
835 let entity_name = entity_name.to_string();
836 let merge_request = MergeRequest::new(&entity_name, target, subordinate, false);
837
838 Ok(request
839 .header("Content-Type", "application/json")
840 .body(serde_json::to_vec(&merge_request).into_dataverse_result()?)
841 )
842 },
843 handle_empty_response,
844 ).await
845 }
846
847 async fn request<E, Fut>(
848 &self,
849 method: Method,
850 url: &str,
851 request_preparer: impl FnOnce(RequestBuilder) -> Result<RequestBuilder>,
852 response_consumer: impl FnOnce(Response) -> Fut,
853 ) -> Result<E>
854 where Fut: Future<Output = Result<E>>{
855 let token = self.auth.get_valid_token().await?;
856
857 let response = request_preparer(self.backend.request(method, url))?
858 .bearer_auth(token)
859 .header("OData-MaxVersion", "4.0")
860 .header("OData-Version", "4.0")
861 .header("Accept", "application/json")
862 .send().await.into_dataverse_result()?;
863
864 response_consumer(response).await
865 }
866
867 fn build_simple_url(&self, table_name: impl Display) -> String {
868 format!("{}api/data/v{}/{}", self.url, VERSION, table_name)
869 }
870
871 fn build_targeted_url(&self, table_name: impl Display, target_id: Uuid) -> String {
872 format!(
873 "{}api/data/v{}/{}({})",
874 self.url,
875 VERSION,
876 table_name,
877 target_id.as_hyphenated()
878 )
879 }
880
881 fn build_retrieve_url(&self, table_name: impl Display, target_id: Uuid, columns: &[&str]) -> String {
882 let mut select = String::new();
883 let mut comma_required = false;
884
885 for column in columns {
886 if comma_required {
887 select.push(',');
888 }
889
890 select.push_str(column);
891 comma_required = true;
892 }
893
894 format!(
895 "{}api/data/v{}/{}({})?$select={}",
896 self.url,
897 VERSION,
898 table_name,
899 target_id.as_hyphenated(),
900 select
901 )
902 }
903
904 fn build_query_url(&self, table_name: impl Display, columns: &[&str], query: &Query) -> String {
905 let mut select = String::new();
906 let mut comma_required = false;
907
908 for column in columns {
909 if comma_required {
910 select.push(',');
911 }
912
913 select.push_str(column);
914 comma_required = true;
915 }
916
917 format!(
918 "{}api/data/v{}/{}{}&$select={}",
919 self.url, VERSION, table_name, query, select
920 )
921 }
922}
923
924async fn handle_empty_response(response: Response) -> Result<()> {
925 if response.status().is_client_error() || response.status().is_server_error() {
926 let error_message = response.text().await.unwrap_or_else(|_| String::from("no error details provided from server"));
927 return Err(DataverseError::new(error_message));
928 }
929
930 Ok(())
931}
932
933/**
934A page of retrieved entites by the `retrieve_multiple()` and `retrieve_next_page()`
935by a client instance
936*/
937#[derive(Debug)]
938pub struct Page<E> {
939 pub entities: Vec<E>,
940 next_link: Option<String>,
941}
942
943impl<E> Page<E> {
944 fn new(entities: Vec<E>, next_link: Option<String>) -> Self {
945 Self {
946 entities,
947 next_link,
948 }
949 }
950
951 /// Indicates if there are more records available in the query after this page
952 pub fn is_incomplete(&self) -> bool {
953 self.next_link.is_some()
954 }
955
956 /// Transforms the page into its content as a `Vec`
957 pub fn into_inner(self) -> Vec<E> {
958 self.entities
959 }
960}
961
962#[derive(Deserialize)]
963struct RetrieveMultipleResult<E> {
964 #[serde(rename = "value")]
965 entities: Vec<E>,
966 #[serde(rename = "@odata.nextLink")]
967 next_link: Option<String>,
968}