Skip to main content

transistor/
http.rs

1#[cfg(feature = "async")]
2use crate::types::response::QueryAsyncResponse;
3#[cfg(not(feature = "async"))]
4use crate::types::response::QueryResponse;
5use crate::types::{
6    error::CruxError,
7    http::{Actions, Order},
8    query::Query,
9    response::{EntityHistoryResponse, EntityTxResponse, TxLogResponse, TxLogsResponse},
10    CruxId,
11};
12use chrono::prelude::*;
13use edn_rs::Edn;
14#[cfg(not(feature = "async"))]
15use reqwest::blocking;
16use reqwest::header::HeaderMap;
17use std::collections::BTreeSet;
18use std::str::FromStr;
19
20static DATE_FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S%Z";
21
22/// `HttpClient` has the `reqwest::blocking::Client`,  the `uri` to query and the `HeaderMap` with
23/// all the possible headers. Default header is `Content-Type: "application/edn"`. Synchronous request.
24pub struct HttpClient {
25    #[cfg(not(feature = "async"))]
26    pub(crate) client: blocking::Client,
27    #[cfg(feature = "async")]
28    pub(crate) client: reqwest::Client,
29    pub(crate) uri: String,
30    pub(crate) headers: HeaderMap,
31}
32
33#[cfg(not(feature = "async"))]
34impl HttpClient {
35    /// Function `tx_log` requests endpoint `/tx-log` via `POST` which allow you to send actions `Action`
36    /// to CruxDB.
37    /// The "write" endpoint, to post transactions.
38    pub fn tx_log(&self, actions: Actions) -> Result<TxLogResponse, CruxError> {
39        if actions.is_empty() {
40            return Err(CruxError::TxLogActionError(
41                "Actions cannot be empty.".to_string(),
42            ));
43        }
44        let body = actions.build();
45
46        let resp = self
47            .client
48            .post(&format!("{}/tx-log", self.uri))
49            .headers(self.headers.clone())
50            .body(body)
51            .send()?
52            .text()?;
53
54        let clean_resp = resp.replace("#inst", "");
55        edn_rs::from_str(&clean_resp).map_err(|e| e.into())
56    }
57
58    /// Function `tx_logs` requests endpoint `/tx-log` via `GET` and returns a list of all transactions
59    pub fn tx_logs(&self) -> Result<TxLogsResponse, CruxError> {
60        let resp = self
61            .client
62            .get(&format!("{}/tx-log", self.uri))
63            .headers(self.headers.clone())
64            .send()?
65            .text()?;
66        TxLogsResponse::from_str(&resp)
67    }
68
69    /// Function `entity` requests endpoint `/entity` via `POST` which retrieves the last document
70    /// in CruxDB.
71    /// Field with `CruxId` is required.
72    /// Response is a `reqwest::Result<edn_rs::Edn>` with the last Entity with that ID.
73    pub fn entity(&self, id: CruxId) -> Result<Edn, CruxError> {
74        let crux_id = edn_rs::to_string(id);
75
76        let mut s = String::new();
77        s.push_str("{:eid ");
78        s.push_str(&crux_id);
79        s.push_str("}");
80
81        let resp = self
82            .client
83            .post(&format!("{}/entity", self.uri))
84            .headers(self.headers.clone())
85            .body(s)
86            .send()?;
87
88        if resp.status().as_u16() < 300 {
89            let resp_body = resp.text()?;
90            let edn_resp = Edn::from_str(&resp_body.replace("#inst", ""));
91            edn_resp.or_else(|_| {
92                Err(CruxError::ResponseFailed(format!(
93                    "entity responded with {} for id \"{}\" ",
94                    500, crux_id
95                )))
96            })
97        } else {
98            Err(CruxError::BadResponse(format!(
99                "entity responded with {} for id \"{}\" ",
100                resp.status().as_u16(),
101                crux_id
102            )))
103        }
104    }
105
106    /// Function `entity_timed` is like `entity` but with two optional fields `transaction_time` and `valid_time` that are of type `Option<DateTime<FixedOffset>>`.
107    pub fn entity_timed(
108        &self,
109        id: CruxId,
110        transaction_time: Option<DateTime<FixedOffset>>,
111        valid_time: Option<DateTime<FixedOffset>>,
112    ) -> Result<Edn, CruxError> {
113        let crux_id = edn_rs::to_string(id);
114
115        let mut s = String::new();
116        s.push_str("{:eid ");
117        s.push_str(&crux_id);
118        s.push_str("}");
119
120        let url = build_timed_url(self.uri.clone(), "entity", transaction_time, valid_time);
121
122        let resp = self
123            .client
124            .post(&url)
125            .headers(self.headers.clone())
126            .body(s)
127            .send()?;
128
129        if resp.status().as_u16() < 300 {
130            let resp_body = resp.text()?;
131            let edn_resp = Edn::from_str(&resp_body.replace("#inst", ""));
132            edn_resp.or_else(|_| {
133                Err(CruxError::ResponseFailed(format!(
134                    "entity-timed responded with {} for id \"{}\" ",
135                    500, crux_id
136                )))
137            })
138        } else {
139            Err(CruxError::BadResponse(format!(
140                "entity-timed responded with {} for id \"{}\" ",
141                resp.status().as_u16(),
142                crux_id
143            )))
144        }
145    }
146
147    /// Function `entity_tx` requests endpoint `/entity-tx` via `POST` which retrieves the docs and tx infos
148    /// for the last document for that ID saved in CruxDB.
149    pub fn entity_tx(&self, id: CruxId) -> Result<EntityTxResponse, CruxError> {
150        let crux_id = edn_rs::to_string(id);
151
152        let mut s = String::new();
153        s.push_str("{:eid ");
154        s.push_str(&crux_id);
155        s.push_str("}");
156
157        let resp = self
158            .client
159            .post(&format!("{}/entity-tx", self.uri))
160            .headers(self.headers.clone())
161            .body(s)
162            .send()?;
163
164        if resp.status().as_u16() < 300 {
165            let resp_body = resp.text()?;
166            EntityTxResponse::from_str(&resp_body.replace("#inst", ""))
167        } else {
168            Err(CruxError::BadResponse(format!(
169                "entity-tx responded with {} for id \"{}\" ",
170                resp.status().as_u16(),
171                crux_id
172            )))
173        }
174    }
175
176    /// Function `entity_tx_timed` is like `entity_tx` but with two optional fields `transaction_time` and `valid_time` that are of type `Option<DateTime<FixedOffset>>`.
177    pub fn entity_tx_timed(
178        &self,
179        id: CruxId,
180        transaction_time: Option<DateTime<FixedOffset>>,
181        valid_time: Option<DateTime<FixedOffset>>,
182    ) -> Result<EntityTxResponse, CruxError> {
183        let crux_id = edn_rs::to_string(id);
184
185        let mut s = String::new();
186        s.push_str("{:eid ");
187        s.push_str(&crux_id);
188        s.push_str("}");
189
190        let url = build_timed_url(self.uri.clone(), "entity-tx", transaction_time, valid_time);
191
192        let resp = self
193            .client
194            .post(&url)
195            .headers(self.headers.clone())
196            .body(s)
197            .send()?;
198
199        if resp.status().as_u16() < 300 {
200            let resp_body = resp.text()?;
201            EntityTxResponse::from_str(&resp_body.replace("#inst", ""))
202        } else {
203            Err(CruxError::BadResponse(format!(
204                "entity-tx responded with {} for id \"{}\" ",
205                resp.status().as_u16(),
206                crux_id
207            )))
208        }
209    }
210
211    /// Function `entity_history` requests endpoint `/entity-history` via `GET` which returns a list with all entity's transaction history.
212    /// It is possible to order it with [`Order`](../types/http/enum.Order.html) , `types::http::Order::Asc` and `types::http::Order:Desc`, (second argument) and to include the document for each transaction with the boolean flag `with_docs` (third argument).
213    pub fn entity_history(
214        &self,
215        hash: String,
216        order: Order,
217        with_docs: bool,
218    ) -> Result<EntityHistoryResponse, CruxError> {
219        let url = format!(
220            "{}/entity-history/{}?sort-order={}&with-docs={}",
221            self.uri,
222            hash,
223            edn_rs::to_string(order),
224            with_docs
225        );
226        let resp = self
227            .client
228            .get(&url)
229            .headers(self.headers.clone())
230            .send()?
231            .text()?;
232
233        EntityHistoryResponse::from_str(&resp.replace("#inst", ""))
234    }
235
236    /// Function `entity_history_timed` is an txtension of the function `entity_history`.
237    /// This function receives as the last argument a vector containing [`TimeHistory`](../types/http/enum.TimeHistory.html)  elements.
238    /// `TimeHistory` can be `ValidTime` or `TransactionTime` and both have optional `DateTime<Utc>` params corresponding to the start-time and end-time to be queried.
239    pub fn entity_history_timed(
240        &self,
241        hash: String,
242        order: Order,
243        with_docs: bool,
244        time: Vec<crate::types::http::TimeHistory>,
245    ) -> Result<EntityHistoryResponse, CruxError> {
246        let url = format!(
247            "{}/entity-history/{}?sort-order={}&with-docs={}{}",
248            self.uri,
249            hash,
250            edn_rs::to_string(order),
251            with_docs,
252            edn_rs::to_string(time).replace("[", "").replace("]", ""),
253        );
254
255        let resp = self
256            .client
257            .get(&url)
258            .headers(self.headers.clone())
259            .send()?
260            .text()?;
261
262        EntityHistoryResponse::from_str(&resp.replace("#inst", ""))
263    }
264
265    /// Function `query` requests endpoint `/query` via `POST` which retrives a Set containing a vector of the values defined by the function [`Query::find` - github example](https://github.com/naomijub/transistor/blob/master/examples/simple_query.rs#L53).
266    /// Argument is a `query` of the type `Query`.
267    pub fn query(&self, query: Query) -> Result<BTreeSet<Vec<String>>, CruxError> {
268        let resp = self
269            .client
270            .post(&format!("{}/query", self.uri))
271            .headers(self.headers.clone())
272            .body(edn_rs::to_string(query))
273            .send()?
274            .text()?;
275
276        let query_response: QueryResponse = edn_rs::from_str(&resp)?;
277
278        Ok(query_response.0)
279    }
280}
281
282#[cfg(feature = "async")]
283impl HttpClient {
284    pub async fn tx_log(&self, actions: Actions) -> Result<TxLogResponse, CruxError> {
285        if actions.is_empty() {
286            return Err(CruxError::TxLogActionError(
287                "Actions cannot be empty.".to_string(),
288            ));
289        }
290
291        let body = actions.build();
292
293        let resp = self
294            .client
295            .post(&format!("{}/tx-log", self.uri))
296            .headers(self.headers.clone())
297            .body(body)
298            .send()
299            .await?
300            .text()
301            .await?;
302
303        edn_rs::from_str(&resp).map_err(|e| e.into())
304    }
305
306    pub async fn tx_logs(&self) -> Result<TxLogsResponse, CruxError> {
307        let resp = self
308            .client
309            .get(&format!("{}/tx-log", self.uri))
310            .headers(self.headers.clone())
311            .send()
312            .await?
313            .text()
314            .await?;
315
316        TxLogsResponse::from_str(&resp)
317    }
318
319    pub async fn entity(&self, id: CruxId) -> Result<Edn, CruxError> {
320        let crux_id = edn_rs::to_string(id);
321
322        let mut s = String::new();
323        s.push_str("{:eid ");
324        s.push_str(&crux_id);
325        s.push_str("}");
326
327        let resp = self
328            .client
329            .post(&format!("{}/entity", self.uri))
330            .headers(self.headers.clone())
331            .body(s)
332            .send()
333            .await?;
334
335        if resp.status().as_u16() < 300 {
336            let resp_body = resp.text().await?;
337            let edn_resp = Edn::from_str(&resp_body.replace("#inst", ""));
338            edn_resp.or_else(|_| {
339                Err(CruxError::ResponseFailed(format!(
340                    "entity responded with {} for id \"{}\" ",
341                    500, crux_id
342                )))
343            })
344        } else {
345            Err(CruxError::BadResponse(format!(
346                "entity responded with {} for id \"{}\" ",
347                resp.status().as_u16(),
348                crux_id
349            )))
350        }
351    }
352
353    pub async fn entity_timed(
354        &self,
355        id: CruxId,
356        transaction_time: Option<DateTime<FixedOffset>>,
357        valid_time: Option<DateTime<FixedOffset>>,
358    ) -> Result<Edn, CruxError> {
359        let crux_id = edn_rs::to_string(id);
360
361        let mut s = String::new();
362        s.push_str("{:eid ");
363        s.push_str(&crux_id);
364        s.push_str("}");
365
366        let url = build_timed_url(self.uri.clone(), "entity", transaction_time, valid_time);
367        let resp = self
368            .client
369            .post(&url)
370            .headers(self.headers.clone())
371            .body(s)
372            .send()
373            .await?;
374
375        if resp.status().as_u16() < 300 {
376            let resp_body = resp.text().await?;
377            let edn_resp = Edn::from_str(&resp_body.replace("#inst", ""));
378            edn_resp.or_else(|_| {
379                Err(CruxError::ResponseFailed(format!(
380                    "entity responded with {} for id \"{}\" ",
381                    500, crux_id
382                )))
383            })
384        } else {
385            Err(CruxError::BadResponse(format!(
386                "entity responded with {} for id \"{}\" ",
387                resp.status().as_u16(),
388                crux_id
389            )))
390        }
391    }
392
393    pub async fn entity_tx(&self, id: CruxId) -> Result<EntityTxResponse, CruxError> {
394        let crux_id = edn_rs::to_string(id);
395        let mut s = String::new();
396        s.push_str("{:eid ");
397        s.push_str(&crux_id);
398        s.push_str("}");
399
400        let resp = self
401            .client
402            .post(&format!("{}/entity-tx", self.uri))
403            .headers(self.headers.clone())
404            .body(s)
405            .send()
406            .await?;
407
408        if resp.status().as_u16() < 300 {
409            let resp_body = resp.text().await?;
410            EntityTxResponse::from_str(&resp_body.replace("#inst", ""))
411        } else {
412            Err(CruxError::BadResponse(format!(
413                "entity-tx responded with {} for id \"{}\" ",
414                resp.status().as_u16(),
415                crux_id
416            )))
417        }
418    }
419
420    pub async fn entity_tx_timed(
421        &self,
422        id: CruxId,
423        transaction_time: Option<DateTime<FixedOffset>>,
424        valid_time: Option<DateTime<FixedOffset>>,
425    ) -> Result<EntityTxResponse, CruxError> {
426        let crux_id = edn_rs::to_string(id);
427        let mut s = String::new();
428        s.push_str("{:eid ");
429        s.push_str(&crux_id);
430        s.push_str("}");
431
432        let url = build_timed_url(self.uri.clone(), "entity-tx", transaction_time, valid_time);
433
434        let resp = self
435            .client
436            .post(&url)
437            .headers(self.headers.clone())
438            .body(s)
439            .send()
440            .await?;
441
442        if resp.status().as_u16() < 300 {
443            let resp_body = resp.text().await?;
444            EntityTxResponse::from_str(&resp_body.replace("#inst", ""))
445        } else {
446            Err(CruxError::BadResponse(format!(
447                "entity-tx responded with {} for id \"{}\" ",
448                resp.status().as_u16(),
449                crux_id
450            )))
451        }
452    }
453
454    pub async fn entity_history(
455        &self,
456        hash: String,
457        order: Order,
458        with_docs: bool,
459    ) -> Result<EntityHistoryResponse, CruxError> {
460        let url = format!(
461            "{}/entity-history/{}?sort-order={}&with-docs={}",
462            self.uri,
463            hash,
464            edn_rs::to_string(order),
465            with_docs
466        );
467        let resp = self
468            .client
469            .get(&url)
470            .headers(self.headers.clone())
471            .send()
472            .await?
473            .text()
474            .await?;
475
476        EntityHistoryResponse::from_str(&resp.replace("#inst", ""))
477    }
478
479    pub async fn entity_history_timed(
480        &self,
481        hash: String,
482        order: Order,
483        with_docs: bool,
484        time: Vec<crate::types::http::TimeHistory>,
485    ) -> Result<EntityHistoryResponse, CruxError> {
486        let url = format!(
487            "{}/entity-history/{}?sort-order={}&with-docs={}{}",
488            self.uri,
489            hash,
490            edn_rs::to_string(order),
491            with_docs,
492            edn_rs::to_string(time).replace("[", "").replace("]", ""),
493        );
494
495        let resp = self
496            .client
497            .get(&url)
498            .headers(self.headers.clone())
499            .send()
500            .await?
501            .text()
502            .await?;
503
504        EntityHistoryResponse::from_str(&resp.replace("#inst", ""))
505    }
506
507    pub async fn query(&self, query: Query) -> Result<BTreeSet<Vec<String>>, CruxError> {
508        let resp = self
509            .client
510            .post(&format!("{}/query", self.uri))
511            .headers(self.headers.clone())
512            .body(edn_rs::to_string(query))
513            .send()
514            .await?
515            .text()
516            .await?;
517
518        let query_response: QueryAsyncResponse = edn_rs::from_str(&resp)?;
519
520        Ok(query_response.0)
521    }
522}
523
524fn build_timed_url(
525    url: String,
526    endpoint: &str,
527    transaction_time: Option<DateTime<FixedOffset>>,
528    valid_time: Option<DateTime<FixedOffset>>,
529) -> String {
530    match (transaction_time, valid_time) {
531        (None, None) => format!("{}/{}", url, endpoint),
532        (Some(tx), None) => format!(
533            "{}/{}?transaction-time={}",
534            url,
535            endpoint,
536            tx.format(DATE_FORMAT).to_string()
537        ),
538        (None, Some(valid)) => format!(
539            "{}/{}?valid-time={}",
540            url,
541            endpoint,
542            valid.format(DATE_FORMAT).to_string()
543        ),
544        (Some(tx), Some(valid)) => format!(
545            "{}/{}?transaction-time={}&valid-time={}",
546            url,
547            endpoint,
548            tx.format(DATE_FORMAT).to_string(),
549            valid.format(DATE_FORMAT).to_string()
550        ),
551    }
552    .replace("+", "%2B")
553}
554
555#[cfg(test)]
556mod http {
557    use crate::client::Crux;
558    use crate::types::http::Actions;
559    use crate::types::http::Order;
560    use crate::types::{
561        query::Query,
562        response::{EntityHistoryElement, EntityHistoryResponse, EntityTxResponse, TxLogResponse},
563        CruxId,
564    };
565    use edn_derive::Serialize;
566    use mockito::mock;
567
568    #[derive(Debug, Clone, Serialize)]
569    #[allow(non_snake_case)]
570    pub struct Person {
571        crux__db___id: CruxId,
572        first_name: String,
573        last_name: String,
574    }
575
576    #[test]
577    fn tx_log() {
578        let _m = mock("POST", "/tx-log")
579        .with_status(200)
580        .match_body("[[:crux.tx/put { :crux.db/id :jorge-3, :first-name \"Michael\", :last-name \"Jorge\", }], [:crux.tx/put { :crux.db/id :manuel-1, :first-name \"Diego\", :last-name \"Manuel\", }]]")
581        .with_header("content-type", "text/plain")
582        .with_body("{:crux.tx/tx-id 8, :crux.tx/tx-time #inst \"2020-07-16T21:53:14.628-00:00\"}")
583        .create();
584
585        let person1 = Person {
586            crux__db___id: CruxId::new("jorge-3"),
587            first_name: "Michael".to_string(),
588            last_name: "Jorge".to_string(),
589        };
590
591        let person2 = Person {
592            crux__db___id: CruxId::new("manuel-1"),
593            first_name: "Diego".to_string(),
594            last_name: "Manuel".to_string(),
595        };
596
597        let actions = Actions::new().append_put(person1).append_put(person2);
598
599        let response = Crux::new("localhost", "4000").http_client().tx_log(actions);
600
601        assert_eq!(response.unwrap(), TxLogResponse::default())
602    }
603
604    #[test]
605    #[should_panic(expected = "TxLogActionError(\"Actions cannot be empty.\")")]
606    fn empty_actions_on_tx_log() {
607        let actions = Actions::new();
608
609        let err = Crux::new("localhost", "4000").http_client().tx_log(actions);
610        err.unwrap();
611    }
612
613    #[test]
614    fn tx_logs() {
615        let _m = mock("GET", "/tx-log")
616        .with_status(200)
617        .with_header("content-type", "application/edn")
618        .with_body("({:crux.tx/tx-id 0, :crux.tx/tx-time #inst \"2020-07-09T23:38:06.465-00:00\", :crux.tx.event/tx-events [[:crux.tx/put \"a15f8b81a160b4eebe5c84e9e3b65c87b9b2f18e\" \"125d29eb3bed1bf51d64194601ad4ff93defe0e2\"]]}{:crux.tx/tx-id 1, :crux.tx/tx-time #inst \"2020-07-09T23:39:33.815-00:00\", :crux.tx.event/tx-events [[:crux.tx/put \"a15f8b81a160b4eebe5c84e9e3b65c87b9b2f18e\" \"1b42e0d5137e3833423f7bb958622bee29f91eee\"]]})")
619        .create();
620
621        let response = Crux::new("localhost", "4000").http_client().tx_logs();
622
623        assert_eq!(response.unwrap().tx_events.len(), 2);
624    }
625
626    #[test]
627    #[should_panic(
628        expected = "DeserializeError(\"The following Edn cannot be deserialized to TxLogs: Symbol(\\\"Holy\\\")\")"
629    )]
630    fn tx_log_error() {
631        let _m = mock("GET", "/tx-log")
632            .with_status(200)
633            .with_header("content-type", "application/edn")
634            .with_body("Holy errors!")
635            .create();
636
637        let _error = Crux::new("localhost", "4000")
638            .http_client()
639            .tx_logs()
640            .unwrap();
641    }
642
643    #[test]
644    fn entity() {
645        let expected_body = "Map(Map({\":crux.db/id\": Key(\":hello-entity\"), \":first-name\": Str(\"Hello\"), \":last-name\": Str(\"World\")}))";
646        let _m = mock("POST", "/entity")
647            .with_status(200)
648            .match_body("{:eid :ivan}")
649            .with_header("content-type", "application/edn")
650            .with_body("{:crux.db/id :hello-entity :first-name \"Hello\", :last-name \"World\"}")
651            .create();
652
653        let id = CruxId::new(":ivan");
654        let edn_body = Crux::new("localhost", "3000")
655            .http_client()
656            .entity(id)
657            .unwrap();
658
659        let resp = format!("{:?}", edn_body);
660        assert_eq!(resp, expected_body);
661    }
662
663    #[test]
664    fn entity_tx() {
665        let expected_body = "{:crux.db/id \"d72ccae848ce3a371bd313865cedc3d20b1478ca\", :crux.db/content-hash \"1828ebf4466f98ea3f5252a58734208cd0414376\", :crux.db/valid-time #inst \"2020-07-19T04:12:13.788-00:00\", :crux.tx/tx-time #inst \"2020-07-19T04:12:13.788-00:00\", :crux.tx/tx-id 28}";
666        let _m = mock("POST", "/entity-tx")
667            .with_status(200)
668            .match_body("{:eid :ivan}")
669            .with_header("content-type", "application/edn")
670            .with_body(expected_body)
671            .create();
672
673        let id = CruxId::new(":ivan");
674        let body = Crux::new("localhost", "3000")
675            .http_client()
676            .entity_tx(id)
677            .unwrap();
678
679        assert_eq!(body, EntityTxResponse::default());
680    }
681
682    #[test]
683    fn simple_query() {
684        let expected_body = "#{[:postgres \"Postgres\" true] [:mysql \"MySQL\" true]}";
685        let _m = mock("POST", "/query")
686            .with_status(200)
687            .with_header("content-type", "application/edn")
688            .with_body(expected_body)
689            .create();
690
691        let query = Query::find(vec!["?p1", "?n", "?s"])
692            .unwrap()
693            .where_clause(vec!["?p1 :name ?n", "?p1 :is-sql ?s", "?p1 :is-sql true"])
694            .unwrap()
695            .build();
696        let body = Crux::new("localhost", "3000")
697            .http_client()
698            .query(query.unwrap())
699            .unwrap();
700
701        let response = format!("{:?}", body);
702        assert_eq!(
703            response,
704            "{[\":mysql\", \"MySQL\", \"true\"], [\":postgres\", \"Postgres\", \"true\"]}"
705        );
706    }
707
708    #[test]
709    fn entity_history() {
710        let expected_body = "({:crux.tx/tx-time \"2020-07-19T04:12:13.788-00:00\", :crux.tx/tx-id 28, :crux.db/valid-time \"2020-07-19T04:12:13.788-00:00\", :crux.db/content-hash  \"1828ebf4466f98ea3f5252a58734208cd0414376\"})";
711        let _m = mock("GET", "/entity-history/ecc6475b7ef9acf689f98e479d539e869432cb5e?sort-order=asc&with-docs=false")
712            .with_status(200)
713            .with_header("content-type", "application/edn")
714            .with_body(expected_body)
715            .create();
716
717        let edn_body = Crux::new("localhost", "3000")
718            .http_client()
719            .entity_history(
720                "ecc6475b7ef9acf689f98e479d539e869432cb5e".to_string(),
721                Order::Asc,
722                false,
723            )
724            .unwrap();
725
726        let expected = EntityHistoryResponse {
727            history: vec![EntityHistoryElement::default()],
728        };
729
730        assert_eq!(edn_body, expected);
731    }
732
733    #[test]
734    fn entity_history_docs() {
735        let expected_body = "({:crux.tx/tx-time \"2020-07-19T04:12:13.788-00:00\", :crux.tx/tx-id 28, :crux.db/valid-time \"2020-07-19T04:12:13.788-00:00\", :crux.db/content-hash  \"1828ebf4466f98ea3f5252a58734208cd0414376\", :crux.db/doc :docs})";
736        let _m = mock("GET", "/entity-history/ecc6475b7ef9acf689f98e479d539e869432cb5e?sort-order=asc&with-docs=true")
737            .with_status(200)
738            .with_header("content-type", "application/edn")
739            .with_body(expected_body)
740            .create();
741
742        let edn_body = Crux::new("localhost", "3000")
743            .http_client()
744            .entity_history(
745                "ecc6475b7ef9acf689f98e479d539e869432cb5e".to_string(),
746                Order::Asc,
747                true,
748            )
749            .unwrap();
750
751        let expected = EntityHistoryResponse {
752            history: vec![EntityHistoryElement::default_docs()],
753        };
754
755        assert_eq!(edn_body, expected);
756    }
757}
758
759#[cfg(test)]
760mod build_url {
761    use super::build_timed_url;
762    use chrono::prelude::*;
763
764    #[test]
765    fn both_times_are_none() {
766        let url = build_timed_url("localhost:3000".to_string(), "entity", None, None);
767
768        assert_eq!(url, "localhost:3000/entity");
769    }
770
771    #[test]
772    fn both_times_are_some() {
773        let url = build_timed_url(
774            "localhost:3000".to_string(),
775            "entity",
776            Some(
777                "2020-08-09T18:05:29.301-03:00"
778                    .parse::<DateTime<FixedOffset>>()
779                    .unwrap(),
780            ),
781            Some(
782                "2020-11-09T18:05:29.301-03:00"
783                    .parse::<DateTime<FixedOffset>>()
784                    .unwrap(),
785            ),
786        );
787
788        assert_eq!(url, "localhost:3000/entity?transaction-time=2020-08-09T18:05:29-03:00&valid-time=2020-11-09T18:05:29-03:00");
789    }
790
791    #[test]
792    fn only_tx_time_is_some() {
793        let url = build_timed_url(
794            "localhost:3000".to_string(),
795            "entity",
796            Some(
797                "2020-08-09T18:05:29.301-03:00"
798                    .parse::<DateTime<FixedOffset>>()
799                    .unwrap(),
800            ),
801            None,
802        );
803
804        assert_eq!(
805            url,
806            "localhost:3000/entity?transaction-time=2020-08-09T18:05:29-03:00"
807        );
808    }
809
810    #[test]
811    fn only_valid_time_is_some() {
812        let url = build_timed_url(
813            "localhost:3000".to_string(),
814            "entity",
815            None,
816            Some(
817                "2020-08-09T18:05:29.301+03:00"
818                    .parse::<DateTime<FixedOffset>>()
819                    .unwrap(),
820            ),
821        );
822
823        assert_eq!(
824            url,
825            "localhost:3000/entity?valid-time=2020-08-09T18:05:29%2B03:00"
826        );
827    }
828}