trailbase_sqlvalue/
lib.rs1#![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 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#[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}