use std::str::FromStr;
use vantage_expressions::{Expression, Expressive};
use crate::{
AnySurrealType, surreal_expr,
types::{SurrealType, SurrealTypeThingMarker},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Thing {
table: String,
id: String,
}
impl Thing {
pub fn new(table: impl Into<String>, id: impl Into<String>) -> Self {
Self {
table: table.into(),
id: id.into(),
}
}
pub fn table(&self) -> &str {
&self.table
}
pub fn id(&self) -> &str {
&self.id
}
}
impl std::fmt::Display for Thing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.table, self.id)
}
}
impl From<String> for Thing {
fn from(s: String) -> Self {
s.parse().unwrap_or_else(|_| Thing::new("_", s))
}
}
impl FromStr for Thing {
type Err = String;
fn from_str(thing_str: &str) -> Result<Self, Self::Err> {
if let Some((table, id)) = thing_str.split_once(':') {
Ok(Self {
table: table.to_string(),
id: id.to_string(),
})
} else {
Err(format!("Invalid thing format: {}", thing_str))
}
}
}
impl SurrealType for Thing {
type Target = SurrealTypeThingMarker;
fn to_cbor(&self) -> ciborium::Value {
ciborium::Value::Tag(
8,
Box::new(ciborium::Value::Array(vec![
ciborium::Value::Text(self.table.clone()),
ciborium::Value::Text(self.id.clone()),
])),
)
}
fn from_cbor(cbor: ciborium::Value) -> Option<Self> {
match cbor {
ciborium::Value::Tag(8, boxed_value) => {
if let ciborium::Value::Array(arr) = *boxed_value
&& arr.len() == 2
&& let (ciborium::Value::Text(table), ciborium::Value::Text(id)) =
(&arr[0], &arr[1])
{
return Some(Thing::new(table.clone(), id.clone()));
}
None
}
ciborium::Value::Text(text) => text.parse().ok(), _ => None,
}
}
}
impl Expressive<AnySurrealType> for Thing {
fn expr(&self) -> Expression<AnySurrealType> {
surreal_expr!(format!("{}:{}", self.table, self.id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{identifier::Identifier, surrealdb::SurrealDB};
use indexmap::IndexMap;
use surreal_client::{SurrealClient, SurrealConnection};
use vantage_expressions::ExprDataSource;
use vantage_types::{Record, TryFromRecord, entity};
const DB_URL: &str = "cbor://localhost:8000/rpc";
const ROOT_USER: &str = "root";
const ROOT_PASS: &str = "root";
const TEST_NAMESPACE: &str = "bakery";
const TEST_DATABASE: &str = "thing_test";
async fn get_client() -> SurrealClient {
SurrealConnection::new()
.url(DB_URL)
.namespace(TEST_NAMESPACE)
.database(TEST_DATABASE)
.auth_root(ROOT_USER, ROOT_PASS)
.with_debug(true)
.connect()
.await
.expect("Failed to connect to SurrealDB")
}
async fn get_surrealdb() -> SurrealDB {
let client = get_client().await;
SurrealDB::new(client)
}
async fn setup_test_data(db: &SurrealDB) -> (Identifier, Identifier, String) {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let country_table = Identifier::new(format!("country_{}", timestamp));
let user_table = Identifier::new(format!("user_{}", timestamp));
let country_id = format!("{}:lv", country_table.expr().template);
let user_id = format!("{}:test_user", user_table.expr().template);
let cleanup_country = surreal_expr!("DELETE {}", (country_table));
let _ = db.execute(&cleanup_country).await;
let cleanup_user = surreal_expr!("DELETE {}", (user_table));
let _ = db.execute(&cleanup_user).await;
let create_country =
surreal_expr!(&format!("CREATE {} SET name = {{}}", country_id), "Latvia");
db.execute(&create_country)
.await
.expect("Failed to create country");
let country_thing = Thing::new(country_table.expr().template, "lv");
let create_user = surreal_expr!(
&format!("CREATE {} SET name = {{}}, country = {{}}", user_id),
"Test User",
(country_thing)
);
db.execute(&create_user)
.await
.expect("Failed to create user");
(country_table, user_table, user_id)
}
#[derive(Debug, PartialEq)]
#[entity(SurrealType)]
struct User {
name: String,
country: Thing,
country_name: String,
}
#[tokio::test]
async fn test_thing_database_integration() {
let db = get_surrealdb().await;
let (_country_table, _user_table, user_id) = setup_test_data(&db).await;
let join_query = surreal_expr!(&format!(
"SELECT VALUE [name, country.name] FROM ONLY {}",
user_id
));
let join_result = db
.execute(&join_query)
.await
.expect("Failed to execute join query");
let names: Vec<String> = join_result
.try_get()
.expect("Failed to convert result to Vec<String>");
assert_eq!(names.len(), 2, "Expected array with 2 elements");
assert_eq!(names[0], "Test User", "Expected user name");
assert_eq!(names[1], "Latvia", "Expected country name");
println!("✅ Thing database integration test passed");
}
#[tokio::test]
async fn test_thing_record_conversion() {
let db = get_surrealdb().await;
let (country_table, _user_table, user_id) = setup_test_data(&db).await;
let query = surreal_expr!(&format!(
"SELECT *, country.name as country_name FROM ONLY {}",
user_id
));
let result = db.execute(&query).await.expect("Failed to execute query");
let index_map: IndexMap<String, AnySurrealType> = result
.try_get()
.expect("Failed to convert result to IndexMap");
let record: Record<AnySurrealType> = Record::from_indexmap(index_map);
let user = User::from_record(record).expect("Failed to convert record to User");
assert_eq!(user.name, "Test User", "Expected user name");
assert_eq!(
user.country.table,
country_table.expr().template,
"Expected country table"
);
assert_eq!(user.country.id, "lv", "Expected country id");
assert_eq!(user.country_name, "Latvia", "Expected country name");
println!("✅ Thing record conversion test passed");
}
}