Skip to content

Commit 008b03b

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

6 files changed

Lines changed: 812 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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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 = base64::write::EncoderWriter::new(&mut buf, &base64::engine::general_purpose::STANDARD);
120+
encoder.write_all(data).unwrap();
121+
}
122+
String::from_utf8(buf).unwrap()
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
129+
#[test]
130+
fn test_base64_encode() {
131+
assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
132+
assert_eq!(base64_encode(&[1, 2, 3, 4, 5]), "AQIDBAU=");
133+
assert_eq!(base64_encode(&[]), "");
134+
}
135+
136+
#[test]
137+
fn test_base64_encode_binary() {
138+
// Test with binary data including null bytes
139+
assert_eq!(base64_encode(&[0, 0, 0]), "AAAA");
140+
assert_eq!(base64_encode(&[255, 255, 255]), "////");
141+
}
142+
143+
#[test]
144+
fn test_base64_encode_large() {
145+
// Test with larger binary data
146+
let data: Vec<u8> = (0..255).collect();
147+
let encoded = base64_encode(&data);
148+
assert!(!encoded.is_empty());
149+
// Verify it's valid base64 (only contains valid chars)
150+
assert!(encoded.chars().all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '='));
151+
}
152+
}

src/error.rs

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

0 commit comments

Comments
 (0)