use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use surrealdb::types::{RecordId, RecordIdKey, SurrealValue};
static TABLE_REGISTRY: LazyLock<Mutex<HashMap<&'static str, &'static str>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub trait HasId {
fn id(&self) -> RecordId;
}
pub trait ModelMeta:
Serialize
+ for<'de> Deserialize<'de>
+ SurrealValue
+ std::fmt::Debug
+ 'static
+ Clone
+ Send
+ Sync
{
fn storage_table() -> &'static str {
Self::table_name()
}
fn table_name() -> &'static str;
fn record_id<T>(id: T) -> RecordId
where
RecordIdKey: From<T>,
{
RecordId::new(Self::storage_table(), id)
}
}
pub trait UniqueLookupMeta {
fn lookup_fields() -> &'static [&'static str];
fn foreign_fields() -> &'static [&'static str] {
&[]
}
}
#[doc(hidden)]
pub trait StoreModelMarker {}
#[async_trait::async_trait]
pub trait ResolveRecordId {
async fn resolve_record_id(&self) -> Result<RecordId>;
}
#[async_trait::async_trait]
impl ResolveRecordId for RecordId {
async fn resolve_record_id(&self) -> Result<RecordId> {
Ok(self.clone())
}
}
#[async_trait::async_trait]
impl ResolveRecordId for &RecordId {
async fn resolve_record_id(&self) -> Result<RecordId> {
Ok((*self).clone())
}
}
pub fn register_table(model: &'static str, table: &'static str) -> &'static str {
let mut registry = TABLE_REGISTRY.lock().unwrap_or_else(|err| err.into_inner());
if let Some(existing) = registry.get(model) {
return existing;
}
registry.insert(model, table);
table
}
pub fn default_table_name(type_name: &str) -> &'static str {
let bare = type_name.rsplit("::").next().unwrap_or(type_name);
let snake = to_snake_case(bare);
Box::leak(snake.into_boxed_str())
}
fn to_snake_case(input: &str) -> String {
let mut out = String::with_capacity(input.len() + 4);
let mut prev_is_lower_or_digit = false;
for ch in input.chars() {
if ch.is_ascii_uppercase() {
if prev_is_lower_or_digit {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
prev_is_lower_or_digit = false;
} else {
out.push(ch);
prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
}
}
out
}
#[cfg(test)]
#[path = "meta_tests.rs"]
mod tests;