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}