trailbase_sqlvalue/
lib.rs

1#![forbid(unsafe_code, clippy::unwrap_used)]
2#![allow(clippy::needless_return)]
3#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
4
5use base64::prelude::*;
6use serde::{Deserialize, Serialize};
7use ts_rs::TS;
8
9#[derive(Debug, Clone, thiserror::Error)]
10pub enum DecodeError {
11  #[error("Base64: {0}")]
12  Base64(base64::DecodeError),
13  #[error("Hex")]
14  Hex,
15}
16
17#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, TS)]
18#[ts(export)]
19pub enum Blob {
20  Array(Vec<u8>),
21  /// NOTE: default for reads, given it has best compression-ratio.
22  Base64UrlSafe(String),
23  Hex(String),
24}
25
26impl Blob {
27  pub fn to_b64_url_safe(&self) -> Result<String, DecodeError> {
28    return Ok(match self {
29      Blob::Array(v) => BASE64_URL_SAFE.encode(v),
30      Blob::Base64UrlSafe(s) => s.clone(),
31      Blob::Hex(s) => BASE64_URL_SAFE.encode(decode_hex(s)?),
32    });
33  }
34
35  pub fn into_b64_url_safe(self) -> Result<String, DecodeError> {
36    return Ok(match self {
37      Blob::Array(v) => BASE64_URL_SAFE.encode(&v),
38      Blob::Base64UrlSafe(s) => s,
39      Blob::Hex(s) => BASE64_URL_SAFE.encode(decode_hex(&s)?),
40    });
41  }
42
43  pub fn to_bytes(&self) -> Result<Vec<u8>, DecodeError> {
44    return Ok(match self {
45      Blob::Array(v) => v.clone(),
46      Blob::Base64UrlSafe(s) => BASE64_URL_SAFE.decode(s).map_err(DecodeError::Base64)?,
47      Blob::Hex(s) => decode_hex(s)?,
48    });
49  }
50
51  pub fn into_bytes(self) -> Result<Vec<u8>, DecodeError> {
52    return Ok(match self {
53      Blob::Array(v) => v,
54      Blob::Base64UrlSafe(s) => BASE64_URL_SAFE.decode(&s).map_err(DecodeError::Base64)?,
55      Blob::Hex(s) => decode_hex(&s)?,
56    });
57  }
58}
59
60/// Mimic's rusqlite's Value but is JS/JSON serializable and supports multiple blob encodings..
61#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, TS)]
62#[ts(export)]
63pub enum SqlValue {
64  Null,
65  Integer(i64),
66  Real(f64),
67  Text(String),
68  Blob(Blob),
69}
70
71impl Default for SqlValue {
72  fn default() -> Self {
73    return SqlValue::Null;
74  }
75}
76
77#[cfg(feature = "rusqlite")]
78impl TryFrom<SqlValue> for rusqlite::types::Value {
79  type Error = DecodeError;
80
81  fn try_from(value: SqlValue) -> Result<Self, Self::Error> {
82    use rusqlite::types::Value;
83
84    return Ok(match value {
85      SqlValue::Null => Value::Null,
86      SqlValue::Integer(v) => Value::Integer(v),
87      SqlValue::Real(v) => Value::Real(v),
88      SqlValue::Text(v) => Value::Text(v),
89      SqlValue::Blob(b) => match b {
90        Blob::Array(v) => Value::Blob(v),
91        Blob::Base64UrlSafe(v) => {
92          Value::Blob(BASE64_URL_SAFE.decode(v).map_err(DecodeError::Base64)?)
93        }
94        Blob::Hex(v) => Value::Blob(decode_hex(&v)?),
95      },
96    });
97  }
98}
99
100#[cfg(feature = "rusqlite")]
101impl From<rusqlite::types::Value> for SqlValue {
102  fn from(value: rusqlite::types::Value) -> Self {
103    use rusqlite::types::Value;
104
105    return match value {
106      Value::Null => SqlValue::Null,
107      Value::Integer(v) => SqlValue::Integer(v),
108      Value::Real(v) => SqlValue::Real(v),
109      Value::Text(v) => SqlValue::Text(v),
110      Value::Blob(v) => SqlValue::Blob(Blob::Base64UrlSafe(BASE64_URL_SAFE.encode(v))),
111    };
112  }
113}
114
115#[cfg(feature = "rusqlite")]
116impl From<&rusqlite::types::Value> for SqlValue {
117  fn from(value: &rusqlite::types::Value) -> Self {
118    use rusqlite::types::Value;
119
120    return match value {
121      Value::Null => SqlValue::Null,
122      Value::Integer(v) => SqlValue::Integer(*v),
123      Value::Real(v) => SqlValue::Real(*v),
124      Value::Text(v) => SqlValue::Text(v.clone()),
125      Value::Blob(v) => SqlValue::Blob(Blob::Base64UrlSafe(BASE64_URL_SAFE.encode(v))),
126    };
127  }
128}
129
130fn decode_hex(s: &str) -> Result<Vec<u8>, DecodeError> {
131  if !s.len().is_multiple_of(2) {
132    return Err(DecodeError::Hex);
133  }
134
135  return (0..s.len())
136    .step_by(2)
137    .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| DecodeError::Hex))
138    .collect();
139}