trailbase_client/
lib.rs

1//! A client library to connect to a TrailBase server via HTTP.
2//!
3//! TrailBase is a sub-millisecond, open-source application server with type-safe APIs, built-in
4//! JS/ES6/TS runtime, realtime, auth, and admin UI built on Rust, SQLite & V8.
5
6#![forbid(unsafe_code, clippy::unwrap_used)]
7#![allow(clippy::needless_return)]
8#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
9
10use eventsource_stream::Eventsource;
11pub use futures::Stream;
12use futures::StreamExt;
13use parking_lot::RwLock;
14use reqwest::header::{self, HeaderMap, HeaderValue};
15use reqwest::{Method, StatusCode};
16use std::borrow::Cow;
17use std::sync::Arc;
18use thiserror::Error;
19use tracing::*;
20
21use serde::de::DeserializeOwned;
22use serde::{Deserialize, Serialize};
23
24// TODO: Don't leak internals and make this non_exhaustive.
25#[derive(Debug, Error)]
26#[non_exhaustive]
27pub enum Error {
28  #[error("HTTP status: {0}")]
29  HttpStatus(StatusCode),
30
31  #[error("MissingRefreshToken")]
32  MissingRefreshToken,
33
34  #[error("RecordSerialization: {0}")]
35  RecordSerialization(serde_json::Error),
36
37  #[error("InvalidToken: {0}")]
38  InvalidToken(jsonwebtoken::errors::Error),
39
40  #[error("InvalidUrl: {0}")]
41  InvalidUrl(url::ParseError),
42
43  // NOTE: This error is leaky but comprehensively unpacking reqwest is unsustainable.
44  #[error("Reqwest: {0}")]
45  OtherReqwest(reqwest::Error),
46}
47
48impl From<reqwest::Error> for Error {
49  fn from(err: reqwest::Error) -> Self {
50    match err.status() {
51      Some(code) => Self::HttpStatus(code),
52      _ => Self::OtherReqwest(err),
53    }
54  }
55}
56
57/// Represents the currently logged-in user.
58#[derive(Clone, Debug)]
59pub struct User {
60  pub sub: String,
61  pub email: String,
62}
63
64/// Holds the tokens minted by the server on login.
65///
66/// It is also the exact JSON serialization format.
67#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
68pub struct Tokens {
69  pub auth_token: String,
70  pub refresh_token: Option<String>,
71  pub csrf_token: Option<String>,
72}
73
74#[derive(Clone, Debug, Default, PartialEq)]
75pub struct Pagination {
76  cursor: Option<String>,
77  limit: Option<usize>,
78  offset: Option<usize>,
79}
80
81impl Pagination {
82  pub fn new() -> Self {
83    return Self::default();
84  }
85
86  pub fn with_limit(mut self, limit: impl Into<Option<usize>>) -> Pagination {
87    self.limit = limit.into();
88    return self;
89  }
90
91  pub fn with_cursor(mut self, cursor: impl Into<Option<String>>) -> Pagination {
92    self.cursor = cursor.into();
93    return self;
94  }
95
96  pub fn with_offset(mut self, offset: impl Into<Option<usize>>) -> Pagination {
97    self.offset = offset.into();
98    return self;
99  }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub enum DbEvent {
104  Update(Option<serde_json::Value>),
105  Insert(Option<serde_json::Value>),
106  Delete(Option<serde_json::Value>),
107  Error(String),
108}
109
110#[derive(Clone, Debug, Deserialize)]
111pub struct ListResponse<T> {
112  pub cursor: Option<String>,
113  pub total_count: Option<usize>,
114  pub records: Vec<T>,
115}
116
117pub trait RecordId<'a> {
118  fn serialized_id(self) -> Cow<'a, str>;
119}
120
121impl RecordId<'_> for String {
122  fn serialized_id(self) -> Cow<'static, str> {
123    return Cow::Owned(self);
124  }
125}
126
127impl<'a> RecordId<'a> for &'a String {
128  fn serialized_id(self) -> Cow<'a, str> {
129    return Cow::Borrowed(self);
130  }
131}
132
133impl<'a> RecordId<'a> for &'a str {
134  fn serialized_id(self) -> Cow<'a, str> {
135    return Cow::Borrowed(self);
136  }
137}
138
139impl RecordId<'_> for i64 {
140  fn serialized_id(self) -> Cow<'static, str> {
141    return Cow::Owned(self.to_string());
142  }
143}
144
145pub trait ReadArgumentsTrait<'a> {
146  fn serialized_id(self) -> Cow<'a, str>;
147  fn expand(&self) -> Option<&Vec<&'a str>>;
148}
149
150impl<'a, T: RecordId<'a>> ReadArgumentsTrait<'a> for T {
151  fn serialized_id(self) -> Cow<'a, str> {
152    return self.serialized_id();
153  }
154
155  fn expand(&self) -> Option<&Vec<&'a str>> {
156    return None;
157  }
158}
159
160#[derive(Clone, Debug, PartialEq)]
161pub struct ReadArguments<'a, T: RecordId<'a>> {
162  id: T,
163  expand: Option<Vec<&'a str>>,
164}
165
166impl<'a, T: RecordId<'a>> ReadArguments<'a, T> {
167  pub fn new(id: T) -> Self {
168    return Self { id, expand: None };
169  }
170
171  pub fn with_expand(mut self, expand: impl AsRef<[&'a str]>) -> Self {
172    self.expand = Some(expand.as_ref().to_vec());
173    return self;
174  }
175}
176
177impl<'a, T: RecordId<'a>> ReadArgumentsTrait<'a> for ReadArguments<'a, T> {
178  fn serialized_id(self) -> Cow<'a, str> {
179    return self.id.serialized_id();
180  }
181
182  fn expand(&self) -> Option<&Vec<&'a str>> {
183    return self.expand.as_ref();
184  }
185}
186
187struct ThinClient {
188  client: reqwest::Client,
189  url: url::Url,
190}
191
192impl ThinClient {
193  async fn fetch<T: Serialize>(
194    &self,
195    path: &str,
196    headers: HeaderMap,
197    method: Method,
198    body: Option<&T>,
199    query_params: Option<&[(Cow<'static, str>, Cow<'static, str>)]>,
200  ) -> Result<reqwest::Response, Error> {
201    assert!(path.starts_with("/"));
202
203    let mut url = self.url.clone();
204    url.set_path(path);
205
206    if let Some(query_params) = query_params {
207      let mut params = url.query_pairs_mut();
208      for (key, value) in query_params {
209        params.append_pair(key, value);
210      }
211    }
212
213    let request = {
214      let mut builder = self.client.request(method, url).headers(headers);
215      if let Some(ref body) = body {
216        let json = serde_json::to_string(body).map_err(Error::RecordSerialization)?;
217        builder = builder.body(json);
218      }
219      builder.build()?
220    };
221
222    return Ok(self.client.execute(request).await?);
223  }
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
227struct JwtTokenClaims {
228  sub: String,
229  iat: i64,
230  exp: i64,
231  email: String,
232  csrf_token: String,
233}
234
235fn decode_auth_token<T: DeserializeOwned>(token: &str) -> Result<T, Error> {
236  let decoding_key = jsonwebtoken::DecodingKey::from_secret(&[]);
237
238  // Don't validate the token, we don't have the secret key. Just deserialize the claims/contents.
239  let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
240  validation.insecure_disable_signature_validation();
241
242  return jsonwebtoken::decode::<T>(token, &decoding_key, &validation)
243    .map(|data| data.claims)
244    .map_err(Error::InvalidToken);
245}
246
247#[derive(Clone)]
248pub struct RecordApi {
249  client: Arc<ClientState>,
250  name: String,
251}
252
253#[derive(Clone, Debug, Default, PartialEq)]
254pub struct ListArguments<'a> {
255  pagination: Pagination,
256  order: Option<Vec<&'a str>>,
257  filters: Option<ValueOrFilterGroup>,
258  expand: Option<Vec<&'a str>>,
259  count: bool,
260}
261
262#[derive(Clone, Copy, Debug, PartialEq)]
263pub enum CompareOp {
264  Equal,
265  NotEqual,
266  GreaterThanEqual,
267  GreaterThan,
268  LessThanEqual,
269  LessThan,
270  Like,
271  Regexp,
272}
273
274impl CompareOp {
275  fn format(&self) -> &'static str {
276    return match self {
277      Self::Equal => "$eq",
278      Self::NotEqual => "$ne",
279      Self::GreaterThanEqual => "$gte",
280      Self::GreaterThan => "$gt",
281      Self::LessThanEqual => "$lte",
282      Self::LessThan => "$lt",
283      Self::Like => "$like",
284      Self::Regexp => "$re",
285    };
286  }
287}
288
289#[derive(Clone, Default, Debug, PartialEq)]
290pub struct Filter {
291  pub column: String,
292  pub op: Option<CompareOp>,
293  pub value: String,
294}
295
296impl Filter {
297  pub fn new(column: impl Into<String>, op: CompareOp, value: impl Into<String>) -> Self {
298    return Self {
299      column: column.into(),
300      op: Some(op),
301      value: value.into(),
302    };
303  }
304}
305
306impl From<Filter> for ValueOrFilterGroup {
307  fn from(value: Filter) -> Self {
308    return ValueOrFilterGroup::Filter(value);
309  }
310}
311
312#[derive(Clone, Debug, PartialEq)]
313pub enum ValueOrFilterGroup {
314  Filter(Filter),
315  And(Vec<ValueOrFilterGroup>),
316  Or(Vec<ValueOrFilterGroup>),
317}
318
319impl<F> From<F> for ValueOrFilterGroup
320where
321  F: Into<Vec<Filter>>,
322{
323  fn from(filters: F) -> Self {
324    return ValueOrFilterGroup::And(
325      filters
326        .into()
327        .into_iter()
328        .map(ValueOrFilterGroup::Filter)
329        .collect(),
330    );
331  }
332}
333
334impl<'a> ListArguments<'a> {
335  pub fn new() -> Self {
336    return ListArguments::default();
337  }
338
339  pub fn with_pagination(mut self, pagination: Pagination) -> Self {
340    self.pagination = pagination;
341    return self;
342  }
343
344  pub fn with_order(mut self, order: impl AsRef<[&'a str]>) -> Self {
345    self.order = Some(order.as_ref().to_vec());
346    return self;
347  }
348
349  pub fn with_filters(mut self, filters: impl Into<ValueOrFilterGroup>) -> Self {
350    self.filters = Some(filters.into());
351    return self;
352  }
353
354  pub fn with_expand(mut self, expand: impl AsRef<[&'a str]>) -> Self {
355    self.expand = Some(expand.as_ref().to_vec());
356    return self;
357  }
358
359  pub fn with_count(mut self, count: bool) -> Self {
360    self.count = count;
361    return self;
362  }
363}
364
365impl RecordApi {
366  pub async fn list<T: DeserializeOwned>(
367    &self,
368    args: ListArguments<'_>,
369  ) -> Result<ListResponse<T>, Error> {
370    type Param = (Cow<'static, str>, Cow<'static, str>);
371    let mut params: Vec<Param> = vec![];
372    if let Some(cursor) = args.pagination.cursor {
373      params.push((Cow::Borrowed("cursor"), Cow::Owned(cursor)));
374    }
375
376    if let Some(limit) = args.pagination.limit {
377      params.push((Cow::Borrowed("limit"), Cow::Owned(limit.to_string())));
378    }
379
380    #[inline]
381    fn to_list(slice: &[&str]) -> String {
382      return slice.join(",");
383    }
384
385    if let Some(order) = args.order {
386      if !order.is_empty() {
387        params.push((Cow::Borrowed("order"), Cow::Owned(to_list(&order))));
388      }
389    }
390
391    if let Some(expand) = args.expand {
392      if !expand.is_empty() {
393        params.push((Cow::Borrowed("expand"), Cow::Owned(to_list(&expand))));
394      }
395    }
396
397    if args.count {
398      params.push((Cow::Borrowed("count"), Cow::Borrowed("true")));
399    }
400
401    fn traverse_filters(params: &mut Vec<Param>, path: String, filter: ValueOrFilterGroup) {
402      match filter {
403        ValueOrFilterGroup::Filter(filter) => {
404          if let Some(op) = filter.op {
405            params.push((
406              Cow::Owned(format!(
407                "{path}[{col}][{op}]",
408                col = filter.column,
409                op = op.format()
410              )),
411              Cow::Owned(filter.value),
412            ));
413          } else {
414            params.push((
415              Cow::Owned(format!("{path}[{col}]", col = filter.column)),
416              Cow::Owned(filter.value),
417            ));
418          }
419        }
420        ValueOrFilterGroup::And(vec) => {
421          for (i, f) in vec.into_iter().enumerate() {
422            traverse_filters(params, format!("{path}[$and][{i}]"), f);
423          }
424        }
425        ValueOrFilterGroup::Or(vec) => {
426          for (i, f) in vec.into_iter().enumerate() {
427            traverse_filters(params, format!("{path}[$or][{i}]"), f);
428          }
429        }
430      }
431    }
432
433    if let Some(filters) = args.filters {
434      traverse_filters(&mut params, "filter".to_string(), filters);
435    }
436
437    let response = self
438      .client
439      .fetch(
440        &format!("/{RECORD_API}/{}", self.name),
441        Method::GET,
442        None::<&()>,
443        Some(&params),
444      )
445      .await?;
446
447    return json(response).await;
448  }
449
450  pub async fn read<'a, T: DeserializeOwned>(
451    &self,
452    args: impl ReadArgumentsTrait<'a>,
453  ) -> Result<T, Error> {
454    let expand = args
455      .expand()
456      .map(|e| vec![(Cow::Borrowed("expand"), Cow::Owned(e.join(",")))]);
457
458    let response = self
459      .client
460      .fetch(
461        &format!(
462          "/{RECORD_API}/{name}/{id}",
463          name = self.name,
464          id = args.serialized_id()
465        ),
466        Method::GET,
467        None::<&()>,
468        expand.as_deref(),
469      )
470      .await?;
471
472    return json(response).await;
473  }
474
475  pub async fn create<T: Serialize>(&self, record: T) -> Result<String, Error> {
476    return Ok(self.create_impl(record).await?.swap_remove(0));
477  }
478
479  pub async fn create_bulk<T: Serialize>(&self, record: &[T]) -> Result<Vec<String>, Error> {
480    return self.create_impl(record).await;
481  }
482
483  async fn create_impl<T: Serialize>(&self, record: T) -> Result<Vec<String>, Error> {
484    let response = self
485      .client
486      .fetch(
487        &format!("/{RECORD_API}/{name}", name = self.name),
488        Method::POST,
489        Some(&record),
490        None,
491      )
492      .await?;
493
494    #[derive(Deserialize)]
495    pub struct RecordIdResponse {
496      pub ids: Vec<String>,
497    }
498
499    return Ok(json::<RecordIdResponse>(response).await?.ids);
500  }
501
502  pub async fn update<'a, T: Serialize>(
503    &self,
504    id: impl RecordId<'a>,
505    record: T,
506  ) -> Result<(), Error> {
507    self
508      .client
509      .fetch(
510        &format!(
511          "/{RECORD_API}/{name}/{id}",
512          name = self.name,
513          id = id.serialized_id()
514        ),
515        Method::PATCH,
516        Some(&record),
517        None,
518      )
519      .await?;
520
521    return Ok(());
522  }
523
524  pub async fn delete<'a>(&self, id: impl RecordId<'a>) -> Result<(), Error> {
525    self
526      .client
527      .fetch(
528        &format!(
529          "/{RECORD_API}/{name}/{id}",
530          name = self.name,
531          id = id.serialized_id()
532        ),
533        Method::DELETE,
534        None::<&()>,
535        None,
536      )
537      .await?;
538
539    return Ok(());
540  }
541
542  pub async fn subscribe<'a, T: RecordId<'a>>(
543    &self,
544    id: T,
545  ) -> Result<impl Stream<Item = DbEvent> + use<T>, Error> {
546    // TODO: Might have to add HeaderValue::from_static("text/event-stream").
547    let response = self
548      .client
549      .fetch(
550        &format!(
551          "/{RECORD_API}/{name}/subscribe/{id}",
552          name = self.name,
553          id = id.serialized_id()
554        ),
555        Method::GET,
556        None::<&()>,
557        None,
558      )
559      .await?;
560
561    return Ok(
562      response
563        .bytes_stream()
564        .eventsource()
565        .filter_map(|event_or| async {
566          if let Ok(event) = event_or {
567            if let Ok(db_event) = serde_json::from_str::<DbEvent>(&event.data) {
568              return Some(db_event);
569            }
570          }
571          return None;
572        }),
573    );
574  }
575}
576
577#[derive(Clone, Debug)]
578struct TokenState {
579  state: Option<(Tokens, JwtTokenClaims)>,
580  headers: HeaderMap,
581}
582
583impl TokenState {
584  fn build(tokens: Option<&Tokens>) -> TokenState {
585    let headers = build_headers(tokens);
586    return TokenState {
587      state: tokens.and_then(|tokens| {
588        let Ok(jwt_token) = decode_auth_token::<JwtTokenClaims>(&tokens.auth_token) else {
589          error!("Failed to decode auth token.");
590          return None;
591        };
592        return Some((tokens.clone(), jwt_token));
593      }),
594      headers,
595    };
596  }
597}
598
599struct ClientState {
600  client: ThinClient,
601  site: String,
602  tokens: RwLock<TokenState>,
603}
604
605impl ClientState {
606  #[inline]
607  async fn fetch<T: Serialize>(
608    &self,
609    path: &str,
610    method: Method,
611    body: Option<&T>,
612    query_params: Option<&[(Cow<'static, str>, Cow<'static, str>)]>,
613  ) -> Result<reqwest::Response, Error> {
614    let (mut headers, refresh_token) = self.extract_headers_and_refresh_token_if_exp();
615    if let Some(refresh_token) = refresh_token {
616      let new_tokens = ClientState::refresh_tokens(&self.client, headers, refresh_token).await?;
617
618      headers = new_tokens.headers.clone();
619      *self.tokens.write() = new_tokens;
620    }
621
622    return Ok(
623      self
624        .client
625        .fetch(path, headers, method, body, query_params)
626        .await?
627        .error_for_status()?,
628    );
629  }
630
631  #[inline]
632  fn extract_headers_and_refresh_token_if_exp(&self) -> (HeaderMap, Option<String>) {
633    #[inline]
634    fn should_refresh(jwt: &JwtTokenClaims) -> bool {
635      return jwt.exp - 60 < now() as i64;
636    }
637
638    let tokens = self.tokens.read();
639    let headers = tokens.headers.clone();
640    return match tokens.state {
641      Some(ref state) if should_refresh(&state.1) => (headers, state.0.refresh_token.clone()),
642      _ => (headers, None),
643    };
644  }
645
646  fn extract_headers_refresh_token(&self) -> Option<(HeaderMap, String)> {
647    let tokens = self.tokens.read();
648    let state = tokens.state.as_ref()?;
649
650    if let Some(ref refresh_token) = state.0.refresh_token {
651      return Some((tokens.headers.clone(), refresh_token.clone()));
652    }
653    return None;
654  }
655
656  async fn refresh_tokens(
657    client: &ThinClient,
658    headers: HeaderMap,
659    refresh_token: String,
660  ) -> Result<TokenState, Error> {
661    #[derive(Serialize)]
662    struct RefreshRequest<'a> {
663      refresh_token: &'a str,
664    }
665
666    let response = client
667      .fetch(
668        &format!("/{AUTH_API}/refresh"),
669        headers,
670        Method::POST,
671        Some(&RefreshRequest {
672          refresh_token: &refresh_token,
673        }),
674        None,
675      )
676      .await?;
677
678    #[derive(Deserialize)]
679    struct RefreshResponse {
680      auth_token: String,
681      csrf_token: Option<String>,
682    }
683
684    let refresh_response: RefreshResponse = json(response).await?;
685    return Ok(TokenState::build(Some(&Tokens {
686      auth_token: refresh_response.auth_token,
687      refresh_token: Some(refresh_token),
688      csrf_token: refresh_response.csrf_token,
689    })));
690  }
691}
692
693#[derive(Clone)]
694pub struct Client {
695  state: Arc<ClientState>,
696}
697
698impl Client {
699  pub fn new(site: &str, tokens: Option<Tokens>) -> Result<Client, Error> {
700    return Ok(Client {
701      state: Arc::new(ClientState {
702        client: ThinClient {
703          client: reqwest::Client::new(),
704          url: url::Url::parse(site).map_err(Error::InvalidUrl)?,
705        },
706        site: site.to_string(),
707        tokens: RwLock::new(TokenState::build(tokens.as_ref())),
708      }),
709    });
710  }
711
712  pub fn site(&self) -> String {
713    return self.state.site.clone();
714  }
715
716  pub fn tokens(&self) -> Option<Tokens> {
717    return self.state.tokens.read().state.as_ref().map(|x| x.0.clone());
718  }
719
720  pub fn user(&self) -> Option<User> {
721    if let Some(state) = &self.state.tokens.read().state {
722      return Some(User {
723        sub: state.1.sub.clone(),
724        email: state.1.email.clone(),
725      });
726    }
727    return None;
728  }
729
730  pub fn records(&self, api_name: &str) -> RecordApi {
731    return RecordApi {
732      client: self.state.clone(),
733      name: api_name.to_string(),
734    };
735  }
736
737  pub async fn refresh(&self) -> Result<(), Error> {
738    let Some((headers, refresh_token)) = self.state.extract_headers_refresh_token() else {
739      return Err(Error::MissingRefreshToken);
740    };
741
742    let new_tokens =
743      ClientState::refresh_tokens(&self.state.client, headers, refresh_token).await?;
744
745    *self.state.tokens.write() = new_tokens;
746    return Ok(());
747  }
748
749  pub async fn login(&self, email: &str, password: &str) -> Result<Tokens, Error> {
750    #[derive(Serialize)]
751    struct Credentials<'a> {
752      email: &'a str,
753      password: &'a str,
754    }
755
756    let response = self
757      .state
758      .fetch(
759        &format!("/{AUTH_API}/login"),
760        Method::POST,
761        Some(&Credentials { email, password }),
762        None,
763      )
764      .await?;
765
766    let tokens: Tokens = json(response).await?;
767    self.update_tokens(Some(&tokens));
768    return Ok(tokens);
769  }
770
771  pub async fn logout(&self) -> Result<(), Error> {
772    #[derive(Serialize)]
773    struct LogoutRequest {
774      refresh_token: String,
775    }
776
777    let response_or = match self.state.extract_headers_refresh_token() {
778      Some((_headers, refresh_token)) => {
779        self
780          .state
781          .fetch(
782            &format!("/{AUTH_API}/logout"),
783            Method::POST,
784            Some(&LogoutRequest { refresh_token }),
785            None,
786          )
787          .await
788      }
789      _ => {
790        self
791          .state
792          .fetch(
793            &format!("/{AUTH_API}/logout"),
794            Method::GET,
795            None::<&()>,
796            None,
797          )
798          .await
799      }
800    };
801
802    self.update_tokens(None);
803
804    return response_or.map(|_| ());
805  }
806
807  fn update_tokens(&self, tokens: Option<&Tokens>) -> TokenState {
808    let state = TokenState::build(tokens);
809
810    *self.state.tokens.write() = state.clone();
811    // _authChange?.call(this, state.state?.$1);
812
813    if let Some(ref s) = state.state {
814      let now = now();
815      if s.1.exp < now as i64 {
816        warn!("Token expired");
817      }
818    }
819
820    return state;
821  }
822}
823
824fn build_headers(tokens: Option<&Tokens>) -> HeaderMap {
825  let mut base = HeaderMap::with_capacity(5);
826  base.insert(
827    header::CONTENT_TYPE,
828    HeaderValue::from_static("application/json"),
829  );
830
831  if let Some(tokens) = tokens {
832    if let Ok(value) = HeaderValue::from_str(&format!("Bearer {}", tokens.auth_token)) {
833      base.insert(header::AUTHORIZATION, value);
834    } else {
835      error!("Failed to build bearer token.");
836    }
837
838    if let Some(ref refresh) = tokens.refresh_token {
839      if let Ok(value) = HeaderValue::from_str(refresh) {
840        base.insert("Refresh-Token", value);
841      } else {
842        error!("Failed to build refresh token header.");
843      }
844    }
845
846    if let Some(ref csrf) = tokens.csrf_token {
847      if let Ok(value) = HeaderValue::from_str(csrf) {
848        base.insert("CSRF-Token", value);
849      } else {
850        error!("Failed to build refresh token header.");
851      }
852    }
853  }
854
855  return base;
856}
857
858fn now() -> u64 {
859  return std::time::SystemTime::now()
860    .duration_since(std::time::UNIX_EPOCH)
861    .expect("Duration since epoch")
862    .as_secs();
863}
864
865#[inline]
866async fn json<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T, Error> {
867  let full = resp.bytes().await?;
868  return serde_json::from_slice(&full).map_err(Error::RecordSerialization);
869}
870
871const AUTH_API: &str = "api/auth/v1";
872const RECORD_API: &str = "api/records/v1";
873
874#[cfg(test)]
875mod tests {
876  use super::*;
877
878  #[tokio::test]
879  async fn is_send_test() {
880    let client = Client::new("http://127.0.0.1:4000", None).unwrap();
881
882    let api = client.records("simple_strict_table");
883
884    for _ in 0..2 {
885      let api = api.clone();
886      tokio::spawn(async move {
887        // This would not compile if locks would be held across async function calls.
888        let response = api.read::<serde_json::Value>(0).await;
889        assert!(response.is_err());
890      })
891      .await
892      .unwrap();
893    }
894  }
895}