Skip to content

Commit e5cbbff

Browse files
committed
feat: add API backing functions
- Added decoder and tests - Added tests for error types
1 parent 0536341 commit e5cbbff

6 files changed

Lines changed: 826 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ futures-core = "0.3.31"
2323
time = "0.3.44"
2424
tokio = { version = "1.48.0", features = ["sync"] }
2525
indexmap = { version = "2.12.1", features = ["serde"] }
26+
base64 = "0.22.1"
2627

2728
# SQLx for types and queries (time feature enables datetime type decoding)
2829
sqlx = { version = "0.8.6", features = ["sqlite", "json", "time", "runtime-tokio"] }

src/decode.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use serde_json::Value as JsonValue;
2+
use sqlx::sqlite::SqliteValueRef;
3+
use sqlx::{TypeInfo, Value, ValueRef};
4+
use time::PrimitiveDateTime;
5+
6+
use crate::Error;
7+
8+
/// Convert a SQLite value to a JSON value.
9+
///
10+
/// This function handles the type conversion from SQLite's native types
11+
/// to JSON-compatible representations.
12+
pub fn to_json(value: SqliteValueRef) -> Result<JsonValue, Error> {
13+
if value.is_null() {
14+
return Ok(JsonValue::Null);
15+
}
16+
17+
let column_type = value.type_info();
18+
19+
// Handle types based on SQLite's type affinity
20+
let result = match column_type.name() {
21+
"TEXT" => {
22+
if let Ok(v) = value.to_owned().try_decode::<String>() {
23+
JsonValue::String(v)
24+
} else {
25+
JsonValue::Null
26+
}
27+
}
28+
29+
"REAL" => {
30+
if let Ok(v) = value.to_owned().try_decode::<f64>() {
31+
JsonValue::from(v)
32+
} else {
33+
JsonValue::Null
34+
}
35+
}
36+
37+
"INTEGER" | "NUMERIC" => {
38+
if let Ok(v) = value.to_owned().try_decode::<i64>() {
39+
JsonValue::Number(v.into())
40+
} else {
41+
JsonValue::Null
42+
}
43+
}
44+
45+
"BOOLEAN" => {
46+
if let Ok(v) = value.to_owned().try_decode::<bool>() {
47+
JsonValue::Bool(v)
48+
} else {
49+
JsonValue::Null
50+
}
51+
}
52+
53+
"DATE" => {
54+
// SQLite stores dates as TEXT in ISO 8601 format
55+
if let Ok(v) = value.to_owned().try_decode::<String>() {
56+
JsonValue::String(v)
57+
} else {
58+
JsonValue::Null
59+
}
60+
}
61+
62+
"TIME" => {
63+
// SQLite stores time as TEXT in HH:MM:SS format
64+
if let Ok(v) = value.to_owned().try_decode::<String>() {
65+
JsonValue::String(v)
66+
} else {
67+
JsonValue::Null
68+
}
69+
}
70+
71+
"DATETIME" => {
72+
// Try to decode as PrimitiveDateTime
73+
if let Ok(dt) = value.to_owned().try_decode::<PrimitiveDateTime>() {
74+
JsonValue::String(dt.to_string())
75+
} else if let Ok(v) = value.to_owned().try_decode::<String>() {
76+
// Fall back to string representation
77+
JsonValue::String(v)
78+
} else {
79+
JsonValue::Null
80+
}
81+
}
82+
83+
"BLOB" => {
84+
if let Ok(blob) = value.to_owned().try_decode::<Vec<u8>>() {
85+
// Encode binary data as base64 for JSON serialization
86+
JsonValue::String(base64_encode(&blob))
87+
} else {
88+
JsonValue::Null
89+
}
90+
}
91+
92+
"NULL" => JsonValue::Null,
93+
94+
_ => {
95+
// For unknown types, try to decode as text
96+
if let Ok(text) = value.to_owned().try_decode::<String>() {
97+
JsonValue::String(text)
98+
} else {
99+
return Err(Error::UnsupportedDatatype(format!(
100+
"Unknown SQLite type: {}",
101+
column_type.name()
102+
)));
103+
}
104+
}
105+
};
106+
107+
Ok(result)
108+
}
109+
110+
/// Base64 encode binary data for JSON serialization.
111+
///
112+
/// SQLite BLOB columns are encoded as base64 strings when serialized to JSON,
113+
/// as JSON does not have a native binary type.
114+
fn base64_encode(data: &[u8]) -> String {
115+
use std::io::Write;
116+
117+
let mut buf = Vec::new();
118+
{
119+
let mut encoder =
120+
base64::write::EncoderWriter::new(&mut buf, &base64::engine::general_purpose::STANDARD);
121+
encoder.write_all(data).unwrap();
122+
}
123+
String::from_utf8(buf).unwrap()
124+
}
125+
126+
#[cfg(test)]
127+
mod tests {
128+
use super::*;
129+
130+
#[test]
131+
fn test_base64_encode() {
132+
assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
133+
assert_eq!(base64_encode(&[1, 2, 3, 4, 5]), "AQIDBAU=");
134+
assert_eq!(base64_encode(&[]), "");
135+
}
136+
137+
#[test]
138+
fn test_base64_encode_binary() {
139+
// Test with binary data including null bytes
140+
assert_eq!(base64_encode(&[0, 0, 0]), "AAAA");
141+
assert_eq!(base64_encode(&[255, 255, 255]), "////");
142+
}
143+
144+
#[test]
145+
fn test_base64_encode_large() {
146+
// Test with larger binary data
147+
let data: Vec<u8> = (0..255).collect();
148+
let encoded = base64_encode(&data);
149+
assert!(!encoded.is_empty());
150+
// Verify it's valid base64 (only contains valid chars)
151+
assert!(
152+
encoded
153+
.chars()
154+
.all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '=')
155+
);
156+
}
157+
}

src/error.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,97 @@ impl Serialize for Error {
8787
response.serialize(serializer)
8888
}
8989
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use super::*;
94+
95+
#[test]
96+
fn test_error_code_database_not_loaded() {
97+
let err = Error::DatabaseNotLoaded("test.db".into());
98+
assert_eq!(err.error_code(), "DATABASE_NOT_LOADED");
99+
}
100+
101+
#[test]
102+
fn test_error_code_invalid_path() {
103+
let err = Error::InvalidPath("/bad/path".into());
104+
assert_eq!(err.error_code(), "INVALID_PATH");
105+
}
106+
107+
#[test]
108+
fn test_error_code_unsupported_datatype() {
109+
let err = Error::UnsupportedDatatype("WEIRD_TYPE".into());
110+
assert_eq!(err.error_code(), "UNSUPPORTED_DATATYPE");
111+
}
112+
113+
#[test]
114+
fn test_error_code_read_only_query() {
115+
let err = Error::ReadOnlyQueryInExecute;
116+
assert_eq!(err.error_code(), "READ_ONLY_QUERY_IN_EXECUTE");
117+
}
118+
119+
#[test]
120+
fn test_error_code_multiple_rows() {
121+
let err = Error::MultipleRowsReturned(5);
122+
assert_eq!(err.error_code(), "MULTIPLE_ROWS_RETURNED");
123+
}
124+
125+
#[test]
126+
fn test_error_serialization_structure() {
127+
let err = Error::DatabaseNotLoaded("mydb.db".into());
128+
let json = serde_json::to_value(&err).unwrap();
129+
130+
// Verify structure has both code and message fields
131+
assert!(json.is_object());
132+
assert!(json.get("code").is_some());
133+
assert!(json.get("message").is_some());
134+
}
135+
136+
#[test]
137+
fn test_error_serialization_database_not_loaded() {
138+
let err = Error::DatabaseNotLoaded("mydb.db".into());
139+
let json = serde_json::to_value(&err).unwrap();
140+
141+
assert_eq!(json["code"], "DATABASE_NOT_LOADED");
142+
assert!(json["message"].as_str().unwrap().contains("mydb.db"));
143+
assert!(json["message"].as_str().unwrap().contains("not loaded"));
144+
}
145+
146+
#[test]
147+
fn test_error_serialization_invalid_path() {
148+
let err = Error::InvalidPath("/bad/path".into());
149+
let json = serde_json::to_value(&err).unwrap();
150+
151+
assert_eq!(json["code"], "INVALID_PATH");
152+
assert!(json["message"].as_str().unwrap().contains("/bad/path"));
153+
}
154+
155+
#[test]
156+
fn test_error_serialization_multiple_rows() {
157+
let err = Error::MultipleRowsReturned(3);
158+
let json = serde_json::to_value(&err).unwrap();
159+
160+
assert_eq!(json["code"], "MULTIPLE_ROWS_RETURNED");
161+
let message = json["message"].as_str().unwrap();
162+
assert!(message.contains("3 rows"));
163+
assert!(message.contains("0 or 1"));
164+
}
165+
166+
#[test]
167+
fn test_error_message_format() {
168+
// Verify error messages are descriptive
169+
let err = Error::MultipleRowsReturned(5);
170+
let message = err.to_string();
171+
assert!(message.contains("fetchOne()"));
172+
assert!(message.contains("5 rows"));
173+
assert!(message.contains("expected 0 or 1"));
174+
}
175+
176+
#[test]
177+
fn test_read_only_error_message() {
178+
let err = Error::ReadOnlyQueryInExecute;
179+
let message = err.to_string();
180+
assert!(message.contains("execute()"));
181+
assert!(message.contains("fetchX()"));
182+
}
183+
}

0 commit comments

Comments
 (0)