use ents::{
check_incoming_edges, DraftError, EdgeDraft, EdgeQuery, EdgeValue, Ent,
EntExt, EntMutationError, Id, IncomingEdgeProvider, IncomingEdgeValue,
NullEdgeProvider, QueryEdge, ReadEnt, Transactional,
};
use ents_admin::AdminEnt;
use ents_sqlite::Txn;
use r2d2_sqlite::rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::any::{Any, TypeId};
use std::collections::HashMap;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct UserNameIndex {
pub id: Id,
pub last_updated: u64,
}
#[typetag::serde]
impl Ent for UserNameIndex {
type EdgeProvider = NullEdgeProvider;
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
fn last_updated(&self) -> u64 {
self.last_updated
}
fn mark_updated(&mut self) -> Result<(), EntMutationError> {
self.last_updated = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
Ok(())
}
}
impl UserNameIndex {
pub fn new() -> Self {
Self {
id: 0,
last_updated: 0,
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct User {
pub username: String,
pub email: String,
pub username_index_id: Id,
pub id: Id,
pub last_updated: u64,
}
#[derive(PartialEq, Debug)]
pub struct UniqueUsernameDraft {
pub user_id: Id,
pub username_index_id: Id,
pub username: String,
}
impl EdgeDraft for UniqueUsernameDraft {
fn check<T: ReadEnt>(
self,
txn: &T,
) -> Result<Vec<IncomingEdgeValue>, DraftError> {
let username_bytes = self.username.as_bytes();
let existing_result = txn.find_edges(
self.username_index_id,
EdgeQuery::asc(&[username_bytes]),
)?;
for edge in &existing_result.edges {
if edge.sort_key == username_bytes {
return Err(DraftError::ValidationFailed(format!(
"Username '{}' is already taken by user {}",
self.username, edge.dest
)));
}
}
Ok(vec![IncomingEdgeValue::new(
self.username_index_id,
username_bytes.to_vec(),
)])
}
}
pub struct UserEdgeProvider;
impl IncomingEdgeProvider<User> for UserEdgeProvider {
type Draft = UniqueUsernameDraft;
fn draft(ent: &User) -> Self::Draft {
UniqueUsernameDraft {
user_id: ent.id(),
username_index_id: ent.username_index_id,
username: ent.username.clone(),
}
}
}
#[typetag::serde]
impl Ent for User {
type EdgeProvider = UserEdgeProvider;
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
fn last_updated(&self) -> u64 {
self.last_updated
}
fn mark_updated(&mut self) -> Result<(), EntMutationError> {
self.last_updated = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
Ok(())
}
}
impl User {
pub fn new(username: String, email: String, username_index_id: Id) -> Self {
Self {
username,
email,
username_index_id,
id: 0,
last_updated: 0,
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Post {
pub title: String,
pub content: String,
pub author_id: Id,
pub tag_ids: Vec<Id>,
pub id: Id,
pub last_updated: u64,
}
#[derive(PartialEq, Debug)]
pub struct AuthorEdgeDraft {
pub post_id: Id,
pub author_id: Id,
}
impl EdgeDraft for AuthorEdgeDraft {
fn check<T: ReadEnt>(
self,
txn: &T,
) -> Result<Vec<IncomingEdgeValue>, DraftError> {
let author = txn.get(self.author_id)?;
if author.is_none() {
return Err(DraftError::SourceNotFound(self.author_id));
}
Ok(vec![IncomingEdgeValue::new(
self.author_id,
b"authored".to_vec(),
)])
}
}
#[derive(PartialEq, Debug)]
pub struct TagsEdgeDraft {
pub post_id: Id,
pub tag_ids: Vec<Id>,
}
impl EdgeDraft for TagsEdgeDraft {
fn check<T: ReadEnt>(
self,
txn: &T,
) -> Result<Vec<IncomingEdgeValue>, DraftError> {
let mut edges = Vec::new();
for tag_id in &self.tag_ids {
let tag = txn.get(*tag_id)?;
if tag.is_none() {
return Err(DraftError::SourceNotFound(*tag_id));
}
edges.push(IncomingEdgeValue::new(*tag_id, b"tagged".to_vec()));
}
Ok(edges)
}
}
pub struct PostEdgeProvider;
impl IncomingEdgeProvider<Post> for PostEdgeProvider {
type Draft = (AuthorEdgeDraft, TagsEdgeDraft);
fn draft(ent: &Post) -> Self::Draft {
(
AuthorEdgeDraft {
post_id: ent.id,
author_id: ent.author_id,
},
TagsEdgeDraft {
post_id: ent.id,
tag_ids: ent.tag_ids.clone(),
},
)
}
}
#[typetag::serde]
impl Ent for Post {
type EdgeProvider = PostEdgeProvider;
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
fn last_updated(&self) -> u64 {
self.last_updated
}
fn mark_updated(&mut self) -> Result<(), EntMutationError> {
self.last_updated = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
Ok(())
}
}
impl Post {
pub fn new(
title: String,
content: String,
author_id: Id,
tag_ids: Vec<Id>,
) -> Self {
Self {
title,
content,
author_id,
tag_ids,
id: 0,
last_updated: 0,
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Comment {
pub content: String,
pub post_id: Id,
pub author_id: Id,
pub id: Id,
pub last_updated: u64,
}
#[derive(PartialEq, Debug)]
pub struct CommentEdgeDraft {
pub comment_id: Id,
pub post_id: Id,
pub author_id: Id,
}
impl EdgeDraft for CommentEdgeDraft {
fn check<T: ReadEnt>(
self,
txn: &T,
) -> Result<Vec<IncomingEdgeValue>, DraftError> {
let post = txn.get(self.post_id)?;
if post.is_none() {
return Err(DraftError::SourceNotFound(self.post_id));
}
let author = txn.get(self.author_id)?;
if author.is_none() {
return Err(DraftError::SourceNotFound(self.author_id));
}
Ok(vec![
IncomingEdgeValue::new(self.post_id, b"has_comment".to_vec()),
IncomingEdgeValue::new(self.author_id, b"commented".to_vec()),
])
}
}
pub struct CommentEdgeProvider;
impl IncomingEdgeProvider<Comment> for CommentEdgeProvider {
type Draft = CommentEdgeDraft;
fn draft(ent: &Comment) -> Self::Draft {
CommentEdgeDraft {
comment_id: ent.id,
post_id: ent.post_id,
author_id: ent.author_id,
}
}
}
#[typetag::serde]
impl Ent for Comment {
type EdgeProvider = CommentEdgeProvider;
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
fn last_updated(&self) -> u64 {
self.last_updated
}
fn mark_updated(&mut self) -> Result<(), EntMutationError> {
self.last_updated = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
Ok(())
}
}
impl Comment {
pub fn new(content: String, post_id: Id, author_id: Id) -> Self {
Self {
content,
post_id,
author_id,
id: 0,
last_updated: 0,
}
}
}
trait EdgeEnumerator: Send + Sync {
fn enumerate_edges(
&self,
ent: &Box<dyn Ent>,
txn: &Txn,
) -> Result<Vec<EdgeValue>, DraftError>;
}
struct TypedEdgeEnumerator<E: Ent> {
_phantom: std::marker::PhantomData<E>,
}
impl<E: Ent> TypedEdgeEnumerator<E> {
fn new() -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
}
impl<E: Ent> EdgeEnumerator for TypedEdgeEnumerator<E> {
fn enumerate_edges(
&self,
ent: &Box<dyn Ent>,
txn: &Txn,
) -> Result<Vec<EdgeValue>, DraftError> {
if let Some(concrete_ent) = ent.as_ent::<E>() {
check_incoming_edges(concrete_ent, txn)
} else {
Err(DraftError::ValidationFailed(
"Failed to downcast entity".to_string(),
))
}
}
}
pub struct EdgeProviderRegistry {
enumerators: HashMap<TypeId, Box<dyn EdgeEnumerator>>,
}
impl EdgeProviderRegistry {
pub fn new() -> Self {
Self {
enumerators: HashMap::new(),
}
}
pub fn register<E: Ent>(&mut self) {
let type_id = TypeId::of::<E>();
let enumerator = Box::new(TypedEdgeEnumerator::<E>::new());
self.enumerators.insert(type_id, enumerator);
}
pub fn enumerate_edges(
&self,
ent: &Box<dyn Ent>,
txn: &Txn,
) -> Result<Vec<EdgeValue>, DraftError> {
let type_id = (&**ent as &dyn Any).type_id();
if let Some(enumerator) = self.enumerators.get(&type_id) {
enumerator.enumerate_edges(ent, txn)
} else {
Err(DraftError::ValidationFailed(format!(
"No EdgeProvider registered for type {:?}",
type_id
)))
}
}
}
fn setup_db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE edges (
source INTEGER NOT NULL,
type BLOB NOT NULL,
dest INTEGER NOT NULL,
PRIMARY KEY (source, type, dest)
)",
[],
)
.unwrap();
conn
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Edge Enumeration Example ===\n");
let conn = setup_db();
let tx = conn.unchecked_transaction()?;
let txn = Txn::new(tx);
let mut registry = EdgeProviderRegistry::new();
registry.register::<UserNameIndex>();
registry.register::<User>();
registry.register::<Post>();
registry.register::<Comment>();
println!(
"Registered EdgeProviders for UserNameIndex, User, Post, and Comment\n"
);
println!("Creating UserNameIndex singleton for username uniqueness...");
let username_index = UserNameIndex::new();
let username_index_id = txn.create(username_index)?;
println!(" Created UserNameIndex (id={})", username_index_id);
println!("\nCreating entities...");
let user1 = User::new(
"alice".to_string(),
"alice@example.com".to_string(),
username_index_id,
);
let user1_id = txn.create(user1)?;
println!(" Created User (id={}): alice", user1_id);
let user2 = User::new(
"bob".to_string(),
"bob@example.com".to_string(),
username_index_id,
);
let user2_id = txn.create(user2)?;
println!(" Created User (id={}): bob", user2_id);
let post = Post::new(
"Hello World".to_string(),
"This is my first post".to_string(),
user1_id,
vec![user2_id], );
let post_id = txn.create(post)?;
println!(" Created Post (id={}): Hello World", post_id);
let comment = Comment::new("Great post!".to_string(), post_id, user2_id);
let comment_id = txn.create(comment)?;
println!(" Created Comment (id={}): Great post!", comment_id);
println!("\n=== Enumerating Edges from Box<dyn Ent> ===\n");
let entities: Vec<Id> =
vec![username_index_id, user1_id, user2_id, post_id, comment_id];
for entity_id in entities {
let ent = txn.get(entity_id)?.expect("Entity should exist");
println!("Entity ID {}: {:?}", entity_id, ent.typetag_name());
match registry.enumerate_edges(&ent, &txn) {
Ok(edges) => {
if edges.is_empty() {
println!(" No edges (IncomingEdgeProvider returns none)");
} else {
println!(" Incoming edges (entity is destination):");
for edge in edges {
let edge_type = String::from_utf8_lossy(&edge.sort_key);
println!(
" {} --[{}]--> {}",
edge.source, edge_type, edge.dest
);
}
}
}
Err(e) => {
println!(" Edge draft validation failed (expected for unique constraints): {}", e);
}
}
println!();
}
println!("=== Testing Username Uniqueness Constraint ===\n");
println!("Attempting to create User with duplicate username 'alice'...");
let duplicate_user = User::new(
"alice".to_string(),
"alice2@example.com".to_string(),
username_index_id,
);
match txn.create(duplicate_user) {
Ok(_) => {
println!(" ERROR: Should have failed due to duplicate username!")
}
Err(e) => println!(" Uniqueness constraint correctly failed: {}", e),
}
println!("\nAttempting to create User with unique username 'charlie'...");
let charlie = User::new(
"charlie".to_string(),
"charlie@example.com".to_string(),
username_index_id,
);
match txn.create(charlie) {
Ok(charlie_id) => {
println!(" Successfully created User (id={}): charlie", charlie_id)
}
Err(e) => println!(" ERROR: Should have succeeded: {}", e),
}
println!("\n=== Querying Username Index Edges ===\n");
println!("Edges from UserNameIndex (id={}):", username_index_id);
let username_result =
txn.find_edges(username_index_id, EdgeQuery::asc(&[]))?;
for edge in &username_result.edges {
let username = String::from_utf8_lossy(&edge.sort_key);
println!(" {} --[{}]--> User {}", edge.source, username, edge.dest);
}
println!("\n=== Validating Edges with Transactions ===\n");
println!("Attempting to create Post with non-existent author...");
let invalid_post = Post::new(
"Invalid Post".to_string(),
"This should fail".to_string(),
9999, vec![],
);
match txn.create(invalid_post) {
Ok(_) => println!(" ERROR: Should have failed!"),
Err(e) => println!(" Validation correctly failed: {}", e),
}
println!("\n=== Querying Created Edges ===\n");
println!("Outgoing edges from Post (source={}):", post_id);
let post_outgoing = txn.find_edges(post_id, EdgeQuery::asc(&[]))?;
if post_outgoing.edges.is_empty() {
println!(" (none - Post has incoming edges, not outgoing)");
}
for edge in &post_outgoing.edges {
let edge_type = String::from_utf8_lossy(&edge.sort_key);
println!(" {} --[{}]--> {}", edge.source, edge_type, edge.dest);
}
println!("\nIncoming edges to Post (dest={}):", post_id);
let post_incoming = txn.find_edges_by_dest(post_id)?;
for edge in &post_incoming {
let edge_type = String::from_utf8_lossy(&edge.sort_key);
println!(" {} --[{}]--> {}", edge.source, edge_type, edge.dest);
}
println!("\nIncoming edges to Comment (dest={}):", comment_id);
let comment_incoming = txn.find_edges_by_dest(comment_id)?;
for edge in &comment_incoming {
let edge_type = String::from_utf8_lossy(&edge.sort_key);
println!(" {} --[{}]--> {}", edge.source, edge_type, edge.dest);
}
println!("\n=== Auditing Entities via AuditEntEdges Trait ===\n");
txn.commit()?;
println!("Main transaction committed.\n");
println!(
"Auditing user 'alice' (id={}) using AuditEntEdges trait...",
user1_id
);
{
let audit_tx = conn.unchecked_transaction()?;
let audit_txn = Txn::new(audit_tx);
match audit_txn.audit_ent_edges::<User>(user1_id) {
Ok(()) => println!(" ✓ User edges are valid!"),
Err(e) => println!(" ✗ User edge audit failed: {}", e),
}
}
println!(
"\nAuditing post 'Hello World' (id={}) using AuditEntEdges trait...",
post_id
);
{
let audit_tx = conn.unchecked_transaction()?;
let audit_txn = Txn::new(audit_tx);
match audit_txn.audit_ent_edges::<Post>(post_id) {
Ok(()) => println!(" ✓ Post edges are valid!"),
Err(e) => println!(" ✗ Post edge audit failed: {}", e),
}
}
println!(
"\nAuditing comment (id={}) using AuditEntEdges trait...",
comment_id
);
{
let audit_tx = conn.unchecked_transaction()?;
let audit_txn = Txn::new(audit_tx);
match audit_txn.audit_ent_edges::<Comment>(comment_id) {
Ok(()) => println!(" ✓ Comment edges are valid!"),
Err(e) => println!(" ✗ Comment edge audit failed: {}", e),
}
}
println!("\nVerification: Database unchanged after audits");
{
let verify_tx = conn.unchecked_transaction()?;
let verify_txn = Txn::new(verify_tx);
let user_result =
verify_txn.find_edges(username_index_id, EdgeQuery::asc(&[]))?;
println!(
" UserNameIndex edges: {} (unchanged)",
user_result.edges.len()
);
}
println!("\n=== Audit Complete ===");
Ok(())
}