1mod api;
40pub mod auth;
41mod err;
42pub mod eval;
43mod grid;
44mod hs_types;
45mod tz;
46mod value_ext;
47
48use api::HaystackUrl;
49pub use api::HisReadRange;
50use chrono::Utc;
51pub use err::{Error, NewSkySparkClientError};
52pub use grid::{Grid, ParseJsonGridError};
53pub use hs_types::{Date, DateTime, Time};
54pub use raystack_core::Coord;
55pub use raystack_core::{is_tag_name, ParseTagNameError, TagName};
56pub use raystack_core::{BasicNumber, Number, ScientificNumber};
57pub use raystack_core::{FromHaysonError, Hayson};
58pub use raystack_core::{Marker, Na, RemoveMarker, Symbol, Uri, Xstr};
59pub use raystack_core::{ParseRefError, Ref};
60use serde_json::json;
61use std::convert::TryInto;
62pub use tz::skyspark_tz_string_to_tz;
63use url::Url;
64pub use value_ext::ValueExt;
65
66type Result<T> = std::result::Result<T, Error>;
67type StdResult<T, E> = std::result::Result<T, E>;
68
69pub(crate) async fn new_auth_token(
70 project_api_url: &Url,
71 reqwest_client: &reqwest::Client,
72 username: &str,
73 password: &str,
74) -> StdResult<String, crate::auth::AuthError> {
75 let mut auth_url = project_api_url.clone();
76 auth_url.set_path("/ui");
77
78 let auth_token = auth::new_auth_token(
79 reqwest_client,
80 auth_url.as_str(),
81 username,
82 password,
83 )
84 .await?;
85
86 Ok(auth_token)
87}
88
89#[derive(Debug)]
91pub struct SkySparkClient {
92 auth_token: String,
93 client: reqwest::Client,
94 username: String,
95 password: String,
96 project_api_url: Url,
97}
98
99impl SkySparkClient {
100 pub async fn new(
112 project_api_url: Url,
113 username: &str,
114 password: &str,
115 ) -> std::result::Result<Self, NewSkySparkClientError> {
116 let client = reqwest::Client::new();
117 Self::new_with_client(project_api_url, username, password, client).await
118 }
119
120 pub async fn new_with_client(
151 project_api_url: Url,
152 username: &str,
153 password: &str,
154 reqwest_client: reqwest::Client,
155 ) -> std::result::Result<Self, NewSkySparkClientError> {
156 let project_api_url = add_backslash_if_necessary(project_api_url);
157
158 if project_api_url.cannot_be_a_base() {
159 let url_err_msg = "the project API URL must be a valid base URL";
160 return Err(NewSkySparkClientError::url(url_err_msg));
161 }
162
163 if !has_valid_path_segments(&project_api_url) {
164 let url_err_msg = "URL must be formatted similarly to http://www.test.com/api/project/";
165 return Err(NewSkySparkClientError::url(url_err_msg));
166 }
167
168 Ok(SkySparkClient {
169 auth_token: new_auth_token(
170 &project_api_url,
171 &reqwest_client,
172 username,
173 password,
174 )
175 .await?,
176 client: reqwest_client,
177 username: username.to_owned(),
178 password: password.to_owned(),
179 project_api_url,
180 })
181 }
182
183 #[cfg(test)]
184 pub(crate) fn test_manually_set_auth_token(&mut self, auth_token: &str) {
185 self.auth_token = auth_token.to_owned();
186 }
187
188 #[cfg(test)]
189 pub(crate) fn test_auth_token(&self) -> &str {
190 &self.auth_token
191 }
192
193 async fn update_auth_token(
194 &mut self,
195 ) -> StdResult<(), crate::auth::AuthError> {
196 let auth_token = new_auth_token(
197 self.project_api_url(),
198 self.client(),
199 &self.username,
200 &self.password,
201 )
202 .await?;
203 self.auth_token = auth_token;
204 Ok(())
205 }
206
207 pub fn client(&self) -> &reqwest::Client {
208 &self.client
209 }
210
211 fn auth_header_value(&self) -> String {
212 format!("BEARER authToken={}", self.auth_token)
213 }
214
215 fn eval_url(&self) -> Url {
216 self.append_to_url("eval")
217 }
218
219 async fn get(&mut self, url: Url) -> Result<Grid> {
220 let res = self.get_response(url.clone()).await?;
221
222 if res.status() == reqwest::StatusCode::FORBIDDEN {
223 self.update_auth_token().await?;
224 let retry_res = self.get_response(url).await?;
225 http_response_to_grid(retry_res).await
226 } else {
227 http_response_to_grid(res).await
228 }
229 }
230
231 async fn get_response(&self, url: Url) -> Result<reqwest::Response> {
232 self.client()
233 .get(url)
234 .header("Accept", "application/json")
235 .header("Authorization", self.auth_header_value())
236 .send()
237 .await
238 .map_err(|err| err.into())
239 }
240
241 async fn post(&mut self, url: Url, grid: &Grid) -> Result<Grid> {
242 let res = self.post_response(url.clone(), grid).await?;
243
244 if res.status() == reqwest::StatusCode::FORBIDDEN {
245 self.update_auth_token().await?;
246 let retry_res = self.post_response(url, grid).await?;
247 http_response_to_grid(retry_res).await
248 } else {
249 http_response_to_grid(res).await
250 }
251 }
252
253 async fn post_response(
254 &self,
255 url: Url,
256 grid: &Grid,
257 ) -> Result<reqwest::Response> {
258 self.client()
259 .post(url)
260 .header("Accept", "application/json")
261 .header("Authorization", self.auth_header_value())
262 .header("Content-Type", "application/json")
263 .body(grid.to_json_string())
264 .send()
265 .await
266 .map_err(|err| err.into())
267 }
268
269 fn append_to_url(&self, s: &str) -> Url {
270 self.project_api_url
271 .join(s)
272 .expect("since url ends with '/' this should never fail")
273 }
274
275 pub fn project_name(&self) -> &str {
277 self.project_api_url
280 .path_segments()
281 .expect("proj api url is a valid base URL so this shouldn't fail")
282 .nth(1)
283 .expect("since URL is valid, the project name should be present")
284 }
285
286 pub fn project_api_url(&self) -> &Url {
288 &self.project_api_url
289 }
290}
291
292pub(crate) fn add_backslash_if_necessary(url: Url) -> Url {
296 let chars = url.as_str().chars().collect::<Vec<_>>();
297 let last_char = chars.last().expect("parsed url should have >= 1 chars");
298 if *last_char != '/' {
299 Url::parse(&(url.to_string() + "/")).expect("adding '/' to the end of a parsable url should create another parsable url")
300 } else {
301 url
302 }
303}
304
305impl SkySparkClient {
306 pub async fn about(&mut self) -> Result<Grid> {
308 self.get(self.about_url()).await
309 }
310
311 pub async fn filetypes(&mut self) -> Result<Grid> {
313 self.get(self.filetypes_url()).await
314 }
315
316 pub async fn his_read(
318 &mut self,
319 id: &Ref,
320 range: &HisReadRange,
321 ) -> Result<Grid> {
322 let row = json!({
323 "id": id.to_hayson(),
324 "range": range.to_json_request_string()
325 });
326 let req_grid = Grid::new_internal(vec![row]);
327
328 self.post(self.his_read_url(), &req_grid).await
329 }
330
331 pub async fn his_write_bool(
333 &mut self,
334 id: &Ref,
335 his_data: &[(DateTime, bool)],
336 ) -> Result<Grid> {
337 let rows = his_data
338 .iter()
339 .map(|(date_time, value)| {
340 json!({
341 "ts": date_time.to_hayson(),
342 "val": value
343 })
344 })
345 .collect();
346
347 let mut req_grid = Grid::new_internal(rows);
348 req_grid.add_ref_to_meta(id);
349
350 self.post(self.his_write_url(), &req_grid).await
351 }
352
353 pub async fn his_write_num(
356 &mut self,
357 id: &Ref,
358 his_data: &[(DateTime, Number)],
359 ) -> Result<Grid> {
360 let rows = his_data
361 .iter()
362 .map(|(date_time, value)| {
363 json!({
364 "ts": date_time.to_hayson(),
365 "val": value.to_hayson(),
366 })
367 })
368 .collect();
369
370 let mut req_grid = Grid::new_internal(rows);
371 req_grid.add_ref_to_meta(id);
372
373 self.post(self.his_write_url(), &req_grid).await
374 }
375
376 pub async fn his_write_str(
378 &mut self,
379 id: &Ref,
380 his_data: &[(DateTime, String)],
381 ) -> Result<Grid> {
382 let rows = his_data
383 .iter()
384 .map(|(date_time, value)| {
385 json!({
386 "ts": date_time.to_hayson(),
387 "val": value
388 })
389 })
390 .collect();
391
392 let mut req_grid = Grid::new_internal(rows);
393 req_grid.add_ref_to_meta(id);
394
395 self.post(self.his_write_url(), &req_grid).await
396 }
397
398 pub async fn utc_his_write_bool(
401 &mut self,
402 id: &Ref,
403 time_zone_name: &str,
404 his_data: &[(chrono::DateTime<Utc>, bool)],
405 ) -> Result<Grid> {
406 let tz = skyspark_tz_string_to_tz(time_zone_name).ok_or_else(|| {
407 Error::TimeZone {
408 err_time_zone: time_zone_name.to_owned(),
409 }
410 })?;
411
412 let rows = his_data
413 .iter()
414 .map(|(date_time, value)| {
415 let date_time: DateTime = date_time.with_timezone(&tz).into();
416 json!({
417 "ts": date_time.to_hayson(),
418 "val": value
419 })
420 })
421 .collect();
422
423 let mut req_grid = Grid::new_internal(rows);
424 req_grid.add_ref_to_meta(id);
425
426 self.post(self.his_write_url(), &req_grid).await
427 }
428
429 pub async fn utc_his_write_num(
434 &mut self,
435 id: &Ref,
436 time_zone_name: &str,
437 his_data: &[(chrono::DateTime<Utc>, Number)],
438 ) -> Result<Grid> {
439 let tz = skyspark_tz_string_to_tz(time_zone_name).ok_or_else(|| {
440 Error::TimeZone {
441 err_time_zone: time_zone_name.to_owned(),
442 }
443 })?;
444
445 let rows = his_data
446 .iter()
447 .map(|(date_time, value)| {
448 let date_time: DateTime = date_time.with_timezone(&tz).into();
449
450 json!({
451 "ts": date_time.to_hayson(),
452 "val": value.to_hayson(),
453 })
454 })
455 .collect();
456
457 let mut req_grid = Grid::new_internal(rows);
458 req_grid.add_ref_to_meta(id);
459
460 self.post(self.his_write_url(), &req_grid).await
461 }
462
463 pub async fn utc_his_write_str(
466 &mut self,
467 id: &Ref,
468 time_zone_name: &str,
469 his_data: &[(chrono::DateTime<Utc>, String)],
470 ) -> Result<Grid> {
471 let tz = skyspark_tz_string_to_tz(time_zone_name).ok_or_else(|| {
472 Error::TimeZone {
473 err_time_zone: time_zone_name.to_owned(),
474 }
475 })?;
476
477 let rows = his_data
478 .iter()
479 .map(|(date_time, value)| {
480 let date_time: DateTime = date_time.with_timezone(&tz).into();
481
482 json!({
483 "ts": date_time.to_hayson(),
484 "val": value,
485 })
486 })
487 .collect();
488
489 let mut req_grid = Grid::new_internal(rows);
490 req_grid.add_ref_to_meta(id);
491
492 self.post(self.his_write_url(), &req_grid).await
493 }
494
495 pub async fn nav(&mut self, nav_id: Option<&Ref>) -> Result<Grid> {
497 let req_grid = match nav_id {
498 Some(nav_id) => {
499 let row = json!({ "navId": nav_id.to_hayson() });
500 Grid::new_internal(vec![row])
501 }
502 None => Grid::new_internal(Vec::new()),
503 };
504
505 self.post(self.nav_url(), &req_grid).await
506 }
507
508 pub async fn ops(&mut self) -> Result<Grid> {
510 self.get(self.ops_url()).await
511 }
512
513 pub async fn read(
516 &mut self,
517 filter: &str,
518 limit: Option<u64>,
519 ) -> Result<Grid> {
520 let row = match limit {
521 Some(integer) => json!({"filter": filter, "limit": integer}),
522 None => json!({ "filter": filter }),
523 };
524
525 let req_grid = Grid::new_internal(vec![row]);
526 self.post(self.read_url(), &req_grid).await
527 }
528
529 pub async fn read_by_ids(&mut self, ids: &[Ref]) -> Result<Grid> {
532 let rows = ids.iter().map(|id| json!({"id": id.to_hayson()})).collect();
533
534 let req_grid = Grid::new_internal(rows);
535 self.post(self.read_url(), &req_grid).await
536 }
537}
538
539impl HaystackUrl for SkySparkClient {
540 fn about_url(&self) -> Url {
541 self.append_to_url("about")
542 }
543
544 fn filetypes_url(&self) -> Url {
545 self.append_to_url("filetypes")
546 }
547
548 fn his_read_url(&self) -> Url {
549 self.append_to_url("hisRead")
550 }
551
552 fn his_write_url(&self) -> Url {
553 self.append_to_url("hisWrite")
554 }
555
556 fn nav_url(&self) -> Url {
557 self.append_to_url("nav")
558 }
559
560 fn ops_url(&self) -> Url {
561 self.append_to_url("ops")
562 }
563
564 fn read_url(&self) -> Url {
565 self.append_to_url("read")
566 }
567}
568
569impl SkySparkClient {
570 pub async fn eval(&mut self, axon_expr: &str) -> Result<Grid> {
571 let row = json!({ "expr": axon_expr });
572 let req_grid = Grid::new_internal(vec![row]);
573 self.post(self.eval_url(), &req_grid).await
574 }
575}
576
577async fn http_response_to_grid(res: reqwest::Response) -> Result<Grid> {
578 let json: serde_json::Value = res.json().await?;
579 let grid: Grid = json.try_into()?;
580
581 if grid.is_error() {
582 Err(Error::Grid { err_grid: grid })
583 } else {
584 Ok(grid)
585 }
586}
587
588pub(crate) fn has_valid_path_segments(project_api_url: &Url) -> bool {
591 if let Some(mut segments) = project_api_url.path_segments() {
592 let api_literal = segments.next();
593 let proj_name = segments.next();
594 let blank = segments.next();
595 let should_be_none = segments.next();
596
597 match (api_literal, proj_name, blank, should_be_none) {
598 (_, Some(""), _, _) => false,
599 (Some("api"), Some(_), Some(""), None) => true,
600 _ => false,
601 }
602 } else {
603 false
604 }
605}
606
607#[cfg(test)]
608mod test {
609 use crate::api::HisReadRange;
610 use crate::SkySparkClient;
611 use crate::ValueExt;
612 use raystack_core::{Number, Ref};
613 use serde_json::json;
614 use url::Url;
615
616 fn project_api_url() -> Url {
617 let url_str =
618 std::env::var("RAYSTACK_SKYSPARK_PROJECT_API_URL").unwrap();
619 Url::parse(&url_str).unwrap()
620 }
621
622 fn username() -> String {
623 std::env::var("RAYSTACK_SKYSPARK_USERNAME").unwrap()
624 }
625
626 fn password() -> String {
627 std::env::var("RAYSTACK_SKYSPARK_PASSWORD").unwrap()
628 }
629
630 async fn new_client() -> SkySparkClient {
631 let username = username();
632 let password = password();
633 let reqwest_client = reqwest::Client::new();
634 SkySparkClient::new_with_client(
635 project_api_url(),
636 &username,
637 &password,
638 reqwest_client,
639 )
640 .await
641 .unwrap()
642 }
643
644 #[tokio::test]
645 async fn about() {
646 let mut client = new_client().await;
647 let grid = client.about().await.unwrap();
648 assert_eq!(grid.rows()[0]["whoami"], json!(username()));
649 }
650
651 #[tokio::test]
652 async fn filetypes() {
653 let mut client = new_client().await;
654 let grid = client.filetypes().await.unwrap();
655 assert!(grid.rows()[0]["dis"].is_string());
656 }
657
658 #[tokio::test]
659 async fn his_read_today() {
660 let range = HisReadRange::Today;
661 his_read(&range).await;
662 }
663
664 #[tokio::test]
665 async fn his_read_yesterday() {
666 let range = HisReadRange::Yesterday;
667 his_read(&range).await;
668 }
669
670 #[tokio::test]
671 async fn his_read_date() {
672 let range =
673 HisReadRange::Date(chrono::NaiveDate::from_ymd(2019, 1, 1).into());
674 his_read(&range).await;
675 }
676
677 #[tokio::test]
678 async fn his_read_date_span() {
679 let range = HisReadRange::DateSpan {
680 start: chrono::NaiveDate::from_ymd(2019, 1, 1).into(),
681 end: chrono::NaiveDate::from_ymd(2019, 1, 2).into(),
682 };
683 his_read(&range).await;
684 }
685
686 #[tokio::test]
687 async fn his_read_date_time_span() {
688 use chrono::{DateTime, Duration};
689 use chrono_tz::Australia::Sydney;
690
691 let start = DateTime::parse_from_rfc3339("2019-01-01T00:00:00+10:00")
692 .unwrap()
693 .with_timezone(&Sydney);
694 let end = start + Duration::days(1);
695 let range = HisReadRange::DateTimeSpan {
696 start: start.into(),
697 end: end.into(),
698 };
699 his_read(&range).await;
700 }
701
702 #[tokio::test]
703 async fn his_read_date_time() {
704 use chrono::DateTime;
705 use chrono_tz::Australia::Sydney;
706
707 let date_time =
708 DateTime::parse_from_rfc3339("2012-10-01T00:00:00+10:00")
709 .unwrap()
710 .with_timezone(&Sydney);
711 let range = HisReadRange::SinceDateTime {
712 date_time: date_time.into(),
713 };
714 his_read(&range).await;
715 }
716
717 #[tokio::test]
718 async fn his_read_date_time_utc() {
719 use chrono::DateTime;
720 use chrono_tz::Etc::UTC;
721
722 let date_time = DateTime::parse_from_rfc3339("2012-10-01T00:00:00Z")
723 .unwrap()
724 .with_timezone(&UTC);
725 let range = HisReadRange::SinceDateTime {
726 date_time: date_time.into(),
727 };
728 his_read(&range).await;
729 }
730
731 async fn his_read(range: &HisReadRange) {
732 let filter = format!("point and his and hisEnd");
733
734 let mut client = new_client().await;
735 let points_grid = client.read(&filter, Some(1)).await.unwrap();
736
737 let point_ref = points_grid.rows()[0]["id"].as_hs_ref().unwrap();
738 let his_grid = client.his_read(&point_ref, &range).await.unwrap();
739
740 assert!(his_grid.meta()["hisStart"].is_hs_date_time());
741 assert!(his_grid.meta()["hisEnd"].is_hs_date_time());
742 }
743
744 async fn get_ref_for_filter(
745 client: &mut SkySparkClient,
746 filter: &str,
747 ) -> Ref {
748 let points_grid = client.read(filter, Some(1)).await.unwrap();
749 let point_ref = points_grid.rows()[0]["id"].as_hs_ref().unwrap();
750 point_ref
751 }
752
753 #[tokio::test]
754 async fn utc_his_write_bool() {
755 use chrono::{DateTime, Duration, NaiveDateTime, Utc};
756
757 let ndt = NaiveDateTime::parse_from_str(
758 "2021-01-10 00:00:00",
759 "%Y-%m-%d %H:%M:%S",
760 )
761 .unwrap();
762
763 let date_time1 = DateTime::from_utc(ndt, Utc);
764 let date_time2 = date_time1 + Duration::minutes(5);
765 let date_time3 = date_time1 + Duration::minutes(10);
766
767 let mut client = new_client().await;
768
769 let id = get_ref_for_filter(
770 &mut client,
771 "continuousIntegrationHisWritePoint and kind == \"Bool\"",
772 )
773 .await;
774 let his_data = vec![
775 (date_time1, false),
776 (date_time2, false),
777 (date_time3, false),
778 ];
779
780 let res = client
781 .utc_his_write_bool(&id, "Sydney", &his_data[..])
782 .await
783 .unwrap();
784 assert_eq!(res.rows().len(), 0);
785 }
786
787 #[tokio::test]
788 async fn his_write_bool() {
789 use chrono::{DateTime, Duration};
790 use chrono_tz::Australia::Sydney;
791
792 let mut client = new_client().await;
793
794 let date_time1 =
795 DateTime::parse_from_rfc3339("2019-08-01T00:00:00+10:00")
796 .unwrap()
797 .with_timezone(&Sydney);
798 let date_time2 = date_time1 + Duration::minutes(5);
799 let date_time3 = date_time1 + Duration::minutes(10);
800
801 let id = get_ref_for_filter(
802 &mut client,
803 "continuousIntegrationHisWritePoint and kind == \"Bool\"",
804 )
805 .await;
806 let his_data = vec![
807 (date_time1.into(), true),
808 (date_time2.into(), false),
809 (date_time3.into(), true),
810 ];
811
812 let res = client.his_write_bool(&id, &his_data[..]).await.unwrap();
813 assert_eq!(res.rows().len(), 0);
814 }
815
816 #[tokio::test]
817 async fn utc_his_write_num() {
818 use chrono::{Duration, NaiveDateTime, Utc};
819
820 let ndt = NaiveDateTime::parse_from_str(
821 "2021-01-10 00:00:00",
822 "%Y-%m-%d %H:%M:%S",
823 )
824 .unwrap();
825
826 let date_time1: chrono::DateTime<Utc> =
827 chrono::DateTime::from_utc(ndt, Utc);
828 let date_time2 = date_time1 + Duration::minutes(5);
829 let date_time3 = date_time1 + Duration::minutes(10);
830
831 let mut client = new_client().await;
832
833 let id = get_ref_for_filter(
834 &mut client,
835 "continuousIntegrationHisWritePoint and kind == \"Number\" and unit",
836 )
837 .await;
838
839 let unit = Some("L/s".to_owned());
840
841 let his_data = vec![
842 (date_time1, Number::new(111.111, unit.clone())),
843 (date_time2, Number::new(222.222, unit.clone())),
844 (date_time3, Number::new(333.333, unit.clone())),
845 ];
846
847 let res = client
848 .utc_his_write_num(&id, "Sydney", &his_data[..])
849 .await
850 .unwrap();
851 assert_eq!(res.rows().len(), 0);
852 }
853
854 #[tokio::test]
855 async fn his_write_num() {
856 use chrono::{DateTime, Duration};
857 use chrono_tz::Australia::Sydney;
858
859 let date_time1 =
860 DateTime::parse_from_rfc3339("2019-08-01T00:00:00+10:00")
861 .unwrap()
862 .with_timezone(&Sydney);
863 let date_time2 = date_time1 + Duration::minutes(5);
864 let date_time3 = date_time1 + Duration::minutes(10);
865
866 let mut client = new_client().await;
867
868 let id = get_ref_for_filter(
869 &mut client,
870 "continuousIntegrationHisWritePoint and kind == \"Number\" and unit",
871 )
872 .await;
873
874 let unit = Some("L/s".to_owned());
875
876 let his_data = vec![
877 (date_time1.into(), Number::new(10.0, unit.clone())),
878 (date_time2.into(), Number::new(15.34, unit.clone())),
879 (date_time3.into(), Number::new(1.234, unit.clone())),
880 ];
881
882 let res = client.his_write_num(&id, &his_data[..]).await.unwrap();
883 assert_eq!(res.rows().len(), 0);
884 }
885
886 #[tokio::test]
887 async fn utc_his_write_num_no_unit() {
888 use chrono::{Duration, NaiveDateTime, Utc};
889
890 let ndt = NaiveDateTime::parse_from_str(
891 "2021-01-10 00:00:00",
892 "%Y-%m-%d %H:%M:%S",
893 )
894 .unwrap();
895
896 let date_time1: chrono::DateTime<Utc> =
897 chrono::DateTime::from_utc(ndt, Utc);
898 let date_time2 = date_time1 + Duration::minutes(5);
899 let date_time3 = date_time1 + Duration::minutes(10);
900
901 let mut client = new_client().await;
902
903 let id = get_ref_for_filter(
904 &mut client,
905 "continuousIntegrationHisWritePoint and kind == \"Number\" and not unit",
906 )
907 .await;
908 let his_data = vec![
909 (date_time1, Number::new_unitless(11.11)),
910 (date_time2, Number::new_unitless(22.22)),
911 (date_time3, Number::new_unitless(33.33)),
912 ];
913
914 let res = client
915 .utc_his_write_num(&id, "Sydney", &his_data[..])
916 .await
917 .unwrap();
918 assert_eq!(res.rows().len(), 0);
919 }
920
921 #[tokio::test]
922 async fn his_write_num_no_unit() {
923 use chrono::{DateTime, Duration};
924 use chrono_tz::Australia::Sydney;
925
926 let date_time1 =
927 DateTime::parse_from_rfc3339("2019-08-01T00:00:00+10:00")
928 .unwrap()
929 .with_timezone(&Sydney);
930 let date_time2 = date_time1 + Duration::minutes(5);
931 let date_time3 = date_time1 + Duration::minutes(10);
932
933 let mut client = new_client().await;
934
935 let id = get_ref_for_filter(
936 &mut client,
937 "continuousIntegrationHisWritePoint and kind == \"Number\" and not unit",
938 )
939 .await;
940
941 let his_data = vec![
942 (date_time1.into(), Number::new_unitless(10.0)),
943 (date_time2.into(), Number::new_unitless(15.34)),
944 (date_time3.into(), Number::new_unitless(1.234)),
945 ];
946
947 let res = client.his_write_num(&id, &his_data[..]).await.unwrap();
948 assert_eq!(res.rows().len(), 0);
949 }
950
951 #[tokio::test]
952 async fn utc_his_write_str() {
953 use chrono::{DateTime, Duration, NaiveDateTime, Utc};
954
955 let ndt = NaiveDateTime::parse_from_str(
956 "2021-01-10 00:00:00",
957 "%Y-%m-%d %H:%M:%S",
958 )
959 .unwrap();
960
961 let date_time1 = DateTime::from_utc(ndt, Utc);
962 let date_time2 = date_time1 + Duration::minutes(5);
963 let date_time3 = date_time1 + Duration::minutes(10);
964
965 let mut client = new_client().await;
966 let id = get_ref_for_filter(
967 &mut client,
968 "continuousIntegrationHisWritePoint and kind == \"Str\"",
969 )
970 .await;
971
972 let his_data = vec![
973 (date_time1, "utc".to_owned()),
974 (date_time2, "data".to_owned()),
975 (date_time3, "here".to_owned()),
976 ];
977
978 let res = client
979 .utc_his_write_str(&id, "Sydney", &his_data[..])
980 .await
981 .unwrap();
982 assert_eq!(res.rows().len(), 0);
983 }
984
985 #[tokio::test]
986 async fn his_write_str() {
987 use chrono::{DateTime, Duration};
988 use chrono_tz::Australia::Sydney;
989
990 let date_time1 =
991 DateTime::parse_from_rfc3339("2019-08-01T00:00:00+10:00")
992 .unwrap()
993 .with_timezone(&Sydney);
994 let date_time2 = date_time1 + Duration::minutes(5);
995 let date_time3 = date_time1 + Duration::minutes(10);
996
997 let mut client = new_client().await;
998 let id = get_ref_for_filter(
999 &mut client,
1000 "continuousIntegrationHisWritePoint and kind == \"Str\"",
1001 )
1002 .await;
1003
1004 let his_data = vec![
1005 (date_time1.into(), "hello".to_owned()),
1006 (date_time2.into(), "world".to_owned()),
1007 (date_time3.into(), "!".to_owned()),
1008 ];
1009
1010 let res = client.his_write_str(&id, &his_data[..]).await.unwrap();
1011 assert_eq!(res.rows().len(), 0);
1012 }
1013
1014 #[tokio::test]
1015 async fn nav_root() {
1016 let mut client = new_client().await;
1017 let grid = client.nav(None).await.unwrap();
1018 assert!(grid.rows()[0]["navId"].is_hs_ref());
1019 }
1020
1021 #[tokio::test]
1022 async fn nav() {
1023 let mut client = new_client().await;
1024 let root_grid = client.nav(None).await.unwrap();
1025 let child_nav_id = root_grid.rows()[0]["navId"].as_hs_ref().unwrap();
1026
1027 let child_grid = client.nav(Some(&child_nav_id)).await.unwrap();
1028 let final_nav_id = child_grid.rows()[0]["navId"].as_hs_ref().unwrap();
1029 assert_ne!(child_nav_id, final_nav_id);
1030 }
1031
1032 #[tokio::test]
1033 async fn ops() {
1034 let mut client = new_client().await;
1035 let grid = client.ops().await.unwrap();
1036 assert_eq!(grid.rows()[0]["def"]["_kind"], "symbol");
1037 }
1038
1039 #[tokio::test]
1040 async fn read_with_no_limit() {
1041 let mut client = new_client().await;
1042 let grid = client.read("point", None).await.unwrap();
1043
1044 assert!(grid.rows()[0]["id"].is_hs_ref());
1045 assert!(grid.rows().len() > 10);
1046 }
1047
1048 #[tokio::test]
1049 async fn read_with_zero_limit() {
1050 let mut client = new_client().await;
1051 let grid = client.read("id", Some(0)).await.unwrap();
1052 assert_eq!(grid.rows().len(), 0);
1053 }
1054
1055 #[tokio::test]
1056 async fn read_with_non_zero_limit() {
1057 let mut client = new_client().await;
1058 let grid = client.read("id", Some(1)).await.unwrap();
1059 assert_eq!(grid.rows().len(), 1);
1060
1061 let grid = client.read("id", Some(3)).await.unwrap();
1062 assert_eq!(grid.rows().len(), 3);
1063 }
1064
1065 #[tokio::test]
1066 async fn read_by_ids_with_no_ids() {
1067 let mut client = new_client().await;
1068 let ids = vec![];
1069 let grid_result = client.read_by_ids(&ids).await;
1070 assert!(grid_result.is_err());
1071 }
1072
1073 #[tokio::test]
1074 async fn read_by_ids_single() {
1075 let mut client = new_client().await;
1076 let grid1 = client.read("id", Some(1)).await.unwrap();
1078 let ref1 = grid1.rows()[0]["id"].as_hs_ref().unwrap().clone();
1079 let ids = vec![ref1];
1080 let grid2 = client.read_by_ids(&ids).await.unwrap();
1081 assert_eq!(grid1, grid2);
1082 }
1083
1084 #[tokio::test]
1085 async fn read_by_ids_multiple() {
1086 let mut client = new_client().await;
1087 let grid1 = client.read("id", Some(2)).await.unwrap();
1089 let ref1 = grid1.rows()[0]["id"].as_hs_ref().unwrap().clone();
1090 let ref2 = grid1.rows()[1]["id"].as_hs_ref().unwrap().clone();
1091
1092 let ids = vec![ref1, ref2];
1093 let grid2 = client.read_by_ids(&ids).await.unwrap();
1094 assert_eq!(grid1, grid2);
1095 }
1096
1097 #[tokio::test]
1098 async fn eval() {
1099 let mut client = new_client().await;
1100 let axon_expr = "readAll(id and mod)[0..1].keepCols([\"id\", \"mod\"])";
1101 let grid = client.eval(axon_expr).await.unwrap();
1102 assert!(grid.rows()[0]["id"].is_hs_ref());
1103 }
1104
1105 #[test]
1106 fn add_backslash_necessary() {
1107 use crate::add_backslash_if_necessary;
1108 let url = Url::parse("http://www.example.com/api/proj").unwrap();
1109 let expected = Url::parse("http://www.example.com/api/proj/").unwrap();
1110 assert_eq!(add_backslash_if_necessary(url), expected);
1111 }
1112
1113 #[test]
1114 fn add_backslash_not_necessary() {
1115 use crate::add_backslash_if_necessary;
1116 let url = Url::parse("http://www.example.com/api/proj/").unwrap();
1117 let expected = Url::parse("http://www.example.com/api/proj/").unwrap();
1118 assert_eq!(add_backslash_if_necessary(url), expected);
1119 }
1120
1121 #[tokio::test]
1122 async fn error_grid() {
1123 use crate::err::Error;
1124
1125 let mut client = new_client().await;
1126 let grid_result = client.eval("reabDDDAll(test").await;
1127
1128 assert!(grid_result.is_err());
1129 let err = grid_result.err().unwrap();
1130
1131 match err {
1132 Error::Grid { err_grid } => {
1133 assert!(err_grid.is_error());
1134 assert!(err_grid.error_trace().is_some());
1135 }
1136 _ => panic!(),
1137 }
1138 }
1139
1140 #[tokio::test]
1141 async fn project_name_works() {
1142 let client = new_client().await;
1143 assert!(client.project_name().len() > 3);
1144 }
1145
1146 #[test]
1147 fn has_valid_path_segments_works() {
1148 use super::has_valid_path_segments;
1149
1150 let good_url = Url::parse("http://www.test.com/api/proj/").unwrap();
1151 assert!(has_valid_path_segments(&good_url));
1152 let bad_url1 = Url::parse("http://www.test.com/api/proj").unwrap();
1153 assert!(!has_valid_path_segments(&bad_url1));
1154 let bad_url2 = Url::parse("http://www.test.com/api/").unwrap();
1155 assert!(!has_valid_path_segments(&bad_url2));
1156 let bad_url3 =
1157 Url::parse("http://www.test.com/api/proj/extra").unwrap();
1158 assert!(!has_valid_path_segments(&bad_url3));
1159 let bad_url4 = Url::parse("http://www.test.com").unwrap();
1160 assert!(!has_valid_path_segments(&bad_url4));
1161 let bad_url5 = Url::parse("http://www.test.com/api//extra").unwrap();
1162 assert!(!has_valid_path_segments(&bad_url5));
1163 }
1164
1165 #[tokio::test]
1166 async fn recovers_from_invalid_auth_token() {
1167 let mut client = new_client().await;
1168
1169 let bad_token = "badauthtoken";
1170
1171 assert_ne!(client.test_auth_token(), bad_token);
1172
1173 let grid1 = client.about().await.unwrap();
1175 assert_eq!(grid1.rows()[0]["whoami"], json!(username()));
1176
1177 client.test_manually_set_auth_token(bad_token);
1178 assert_eq!(client.test_auth_token(), bad_token);
1179
1180 let grid2 = client.about().await.unwrap();
1182 assert_eq!(grid2.rows()[0]["whoami"], json!(username()));
1183 }
1184}