use bytes::Bytes;
use serde_json::{json, Value as JsonValue};
use crate::effect::{HttpResponse, Row, SqlValue};
use crate::error::PortError as DriverPortError;
use crate::tick::{PortError, PortOutcome, ReplyBytes};
pub fn http_result_to_outcome(result: Result<HttpResponse, DriverPortError>) -> PortOutcome {
match result {
Ok(response) => PortOutcome::Ok(ReplyBytes(encode_http_response(&response))),
Err(error) => PortOutcome::Err(classify_port_error(error)),
}
}
pub fn encode_http_response(response: &HttpResponse) -> Bytes {
let value = json!({
"status": response.status,
"headers": response.headers,
"body": base64_encode(&response.body),
});
serde_json::to_vec(&value)
.map(Bytes::from)
.unwrap_or_default()
}
pub fn rows_to_reply_bytes(rows: &[Row]) -> Bytes {
let json_rows: Vec<JsonValue> = rows
.iter()
.map(|row| {
let object = row
.iter()
.map(|(key, value)| {
let value = match value {
SqlValue::Text(value) => JsonValue::String(value.clone()),
SqlValue::Integer(value) => json!(value),
SqlValue::Real(value) => json!(value),
SqlValue::Blob(value) => json!(value),
SqlValue::Null => JsonValue::Null,
};
(key.clone(), value)
})
.collect();
JsonValue::Object(object)
})
.collect();
serde_json::to_vec(&json_rows)
.map(Bytes::from)
.unwrap_or_default()
}
pub fn rows_from_reply_bytes(bytes: &Bytes) -> Result<Vec<Row>, String> {
let json: Vec<serde_json::Map<String, JsonValue>> =
serde_json::from_slice(bytes).map_err(|error| error.to_string())?;
Ok(json
.into_iter()
.map(|object| {
object
.into_iter()
.map(|(key, value)| {
let value = match value {
JsonValue::String(value) => SqlValue::Text(value),
JsonValue::Number(value) => value.as_i64().map_or_else(
|| SqlValue::Real(value.as_f64().unwrap_or(0.0)),
SqlValue::Integer,
),
JsonValue::Null => SqlValue::Null,
JsonValue::Array(values) => SqlValue::Blob(
values
.iter()
.filter_map(|value| value.as_u64().map(|value| value as u8))
.collect(),
),
other => SqlValue::Text(other.to_string()),
};
(key, value)
})
.collect()
})
.collect())
}
pub fn classify_port_error(error: DriverPortError) -> PortError {
match &error {
DriverPortError::Storage(_) => PortError::Storage(0),
DriverPortError::Transport(detail) if detail.starts_with("timeout") => PortError::Timeout,
DriverPortError::Transport(_) => PortError::Network,
DriverPortError::Http(detail) => PortError::Http(
detail
.split_whitespace()
.find_map(|word| word.parse::<u16>().ok())
.unwrap_or(0),
),
_ => PortError::Other(0),
}
}
pub fn base64_encode(bytes: &[u8]) -> String {
const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut output = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let first = chunk[0] as u32;
let second = chunk.get(1).copied().unwrap_or_default() as u32;
let third = chunk.get(2).copied().unwrap_or_default() as u32;
let value = (first << 16) | (second << 8) | third;
output.push(TABLE[((value >> 18) & 63) as usize] as char);
output.push(TABLE[((value >> 12) & 63) as usize] as char);
output.push(
chunk
.get(1)
.map_or('=', |_| TABLE[((value >> 6) & 63) as usize] as char),
);
output.push(
chunk
.get(2)
.map_or('=', |_| TABLE[(value & 63) as usize] as char),
);
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_envelope_keeps_binary_body_and_repeated_headers() {
let encoded = encode_http_response(&HttpResponse {
status: 418,
headers: vec![
("set-cookie".into(), "a=1".into()),
("set-cookie".into(), "b=2".into()),
],
body: Bytes::from_static(&[0, 1, 2, 255]),
});
let value: JsonValue = serde_json::from_slice(&encoded).expect("valid envelope");
assert_eq!(value["status"], 418);
assert_eq!(value["headers"].as_array().map(Vec::len), Some(2));
assert_eq!(value["body"], "AAEC/w==");
}
#[test]
fn row_codec_round_trips_every_sql_value() {
let rows = vec![vec![
("text".into(), SqlValue::Text("helix".into())),
("integer".into(), SqlValue::Integer(7)),
("real".into(), SqlValue::Real(1.5)),
("blob".into(), SqlValue::Blob(vec![0, 127, 255])),
("null".into(), SqlValue::Null),
]];
let mut decoded = rows_from_reply_bytes(&rows_to_reply_bytes(&rows)).expect("row decode");
let mut expected = rows;
for row in &mut decoded {
row.sort_unstable_by(|left, right| left.0.cmp(&right.0));
}
for row in &mut expected {
row.sort_unstable_by(|left, right| left.0.cmp(&right.0));
}
assert_eq!(format!("{decoded:?}"), format!("{expected:?}"));
}
#[test]
fn error_mapping_is_shared_by_all_drivers() {
assert_eq!(
classify_port_error(DriverPortError::Transport("timeout: slow".into())),
PortError::Timeout
);
assert_eq!(
classify_port_error(DriverPortError::Http("http status 503".into())),
PortError::Http(503)
);
}
}