raystack/
lib.rs

1//! # Overview
2//! This crate provides functions which can query a SkySpark server, using
3//! the Haystack REST API and the SkySpark REST API's `eval` operation.
4//! Some Haystack operations are not implemented
5//! (watch* operations, pointWrite and invokeAction).
6//!
7//! # Example Usage
8//! Put this in the main function in the `main.rs` file to create
9//! and use a `SkySparkClient`:
10//!
11//! ```rust,no_run
12//! # async fn run() {
13//! use raystack::{SkySparkClient, ValueExt};
14//! use url::Url;
15//!
16//! let url = Url::parse("https://www.example.com/api/projName/").unwrap();
17//! let mut client = SkySparkClient::new(url, "username", "p4ssw0rd").await.unwrap();
18//! let sites_grid = client.eval("readAll(site)").await.unwrap();
19//!
20//! // Print the raw JSON:
21//! println!("{}", sites_grid.to_json_string_pretty());
22//!
23//! // Working with the Grid struct:
24//! println!("All columns: {:?}", sites_grid.cols());
25//! println!("first site id: {:?}", sites_grid.rows()[0]["id"].as_hs_ref().unwrap());
26//! # }
27//! ```
28//!
29//! See the `examples` folder for more usage examples.
30//!
31//! The Grid struct is a wrapper around the underlying JSON Value enum
32//! provided by the `serde_json` crate. See the
33//! [documentation for Value](https://docs.serde.rs/serde_json/enum.Value.html)
34//! for more information on how to query for data stored within it.
35//!
36//! Additional functions for extracting Haystack values from the underlying
37//! JSON are found in this crate's `ValueExt` trait.
38
39mod 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/// A client for interacting with a SkySpark server.
90#[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    /// Create a new `SkySparkClient`.
101    ///
102    /// # Example
103    /// ```rust,no_run
104    /// # async fn run() {
105    /// use raystack::SkySparkClient;
106    /// use url::Url;
107    /// let url = Url::parse("https://skyspark.company.com/api/bigProject/").unwrap();
108    /// let mut client = SkySparkClient::new(url, "username", "p4ssw0rd").await.unwrap();
109    /// # }
110    /// ```
111    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    /// Create a new `SkySparkClient`, passing in an existing
121    /// `reqwest::Client`.
122    ///
123    /// # Example
124    /// ```rust,no_run
125    /// # async fn run() {
126    /// use raystack::SkySparkClient;
127    /// use reqwest::Client;
128    /// use url::Url;
129    /// let reqwest_client = Client::new();
130    /// let url = Url::parse("https://skyspark.company.com/api/bigProject/").unwrap();
131    /// let mut client = SkySparkClient::new_with_client(url, "username", "p4ssw0rd", reqwest_client).await.unwrap();
132    /// # }
133    /// ```
134    ///
135    /// If creating multiple `SkySparkClient`s,
136    /// the same `reqwest::Client` should be used for each. For example:
137    ///
138    /// ```rust,no_run
139    /// # async fn run() {
140    /// use raystack::SkySparkClient;
141    /// use reqwest::Client;
142    /// use url::Url;
143    /// let reqwest_client = Client::new();
144    /// let url1 = Url::parse("http://test.com/api/bigProject/").unwrap();
145    /// let client1 = SkySparkClient::new_with_client(url1, "name", "password", reqwest_client.clone()).await.unwrap();
146    /// let url2 = Url::parse("http://test.com/api/smallProj/").unwrap();
147    /// let client2 = SkySparkClient::new_with_client(url2, "name", "password", reqwest_client.clone()).await.unwrap();
148    /// # }
149    /// ```
150    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    /// Return the project name for this client.
276    pub fn project_name(&self) -> &str {
277        // Since the URL is validated by the `SkySparkClient::new` function,
278        // the following code shouldn't panic:
279        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    /// Return the project API url being used by this client.
287    pub fn project_api_url(&self) -> &Url {
288        &self.project_api_url
289    }
290}
291
292/// If the given url ends with a backslash, return the url without
293/// any modifications. If the given url does not end with a backslash,
294/// append a backslash to the end and return a new `Url`.
295pub(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    /// Returns a grid containing basic server information.
307    pub async fn about(&mut self) -> Result<Grid> {
308        self.get(self.about_url()).await
309    }
310
311    /// Returns a grid describing what MIME types are available.
312    pub async fn filetypes(&mut self) -> Result<Grid> {
313        self.get(self.filetypes_url()).await
314    }
315
316    /// Returns a grid of history data for a single point.
317    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    /// Writes boolean values to a single point.
332    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    /// Writes numeric values to a single point. `unit` must be a valid
354    /// Haystack unit literal, such as `L/s` or `celsius`.
355    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    /// Writes string values to a single point.
377    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    /// Writes boolean values with UTC timestamps to a single point.
399    /// `time_zone_name` must be a valid SkySpark timezone name.
400    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    /// Writes numeric values with UTC timestamps to a single point.
430    /// `unit` must be a valid Haystack unit literal, such as `L/s` or
431    /// `celsius`.
432    /// `time_zone_name` must be a valid SkySpark timezone name.
433    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    /// Writes string values with UTC timestamps to a single point.
464    /// `time_zone_name` must be a valid SkySpark timezone name.
465    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    /// The Haystack nav operation.
496    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    /// Returns a grid containing the operations available on the server.
509    pub async fn ops(&mut self) -> Result<Grid> {
510        self.get(self.ops_url()).await
511    }
512
513    /// Returns a grid containing the records matching the given Axon
514    /// filter string.
515    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    /// Returns a grid containing the records matching the given id
530    /// `Ref`s.
531    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
588/// Returns true if the given URL appears to have the correct path
589/// segments for a SkySpark API URL. The URL should end with a '/' character.
590pub(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        // Get some valid ids:
1077        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        // Get some valid ids:
1088        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        // Check the client works before modifying the auth token:
1174        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        // Check the client still works after setting a bad auth token:
1181        let grid2 = client.about().await.unwrap();
1182        assert_eq!(grid2.rows()[0]["whoami"], json!(username()));
1183    }
1184}