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
22pub 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 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 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 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 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 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 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 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 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 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}