A Rust application to showcase badge awards in the AT Protocol ecosystem.
at main 215 lines 7.2 kB view raw
1//! Database storage implementations for badges, awards, and identities. 2//! 3//! Provides trait-based storage abstraction with SQLite and PostgreSQL backends, 4//! plus file storage for badge images with local and S3 support. 5 6use crate::errors::Result; 7use async_trait::async_trait; 8use chrono::{DateTime, Utc}; 9use serde::{Deserialize, Serialize}; 10use serde_json::Value; 11 12// Re-export storage implementations 13/// File storage implementations for badge images. 14pub mod file_storage; 15#[cfg(feature = "postgres")] 16/// PostgreSQL storage implementation. 17pub mod postgres; 18#[cfg(feature = "sqlite")] 19/// SQLite storage implementation. 20pub mod sqlite; 21 22#[cfg(feature = "s3")] 23pub use file_storage::S3FileStorage; 24pub use file_storage::{FileStorage, LocalFileStorage}; 25#[cfg(feature = "postgres")] 26pub use postgres::*; 27#[cfg(feature = "sqlite")] 28pub use sqlite::*; 29 30/// Identity information for a DID. 31#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 32pub struct Identity { 33 /// Decentralized identifier. 34 pub did: String, 35 /// AT Protocol handle. 36 pub handle: String, 37 /// Full DID document JSON. 38 pub record: Value, 39 /// Record creation timestamp. 40 pub created_at: DateTime<Utc>, 41 /// Record last update timestamp. 42 pub updated_at: DateTime<Utc>, 43} 44 45/// Badge award record. 46#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 47pub struct Award { 48 /// AT-URI of the award record. 49 pub aturi: String, 50 /// Content identifier for the award record. 51 pub cid: String, 52 /// DID of the award recipient. 53 pub did: String, 54 /// AT-URI of the associated badge. 55 pub badge: String, 56 /// Content identifier of the badge record. 57 pub badge_cid: String, 58 /// Human-readable badge name. 59 pub badge_name: String, 60 /// JSON array of validated issuer DIDs. 61 pub validated_issuers: Value, 62 /// Record creation timestamp. 63 pub created_at: DateTime<Utc>, 64 /// Record last update timestamp. 65 pub updated_at: DateTime<Utc>, 66 /// Full JSON record of the award. 67 pub record: Value, 68} 69 70/// Badge definition record. 71#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 72pub struct Badge { 73 /// AT-URI of the badge definition. 74 pub aturi: String, 75 /// Content identifier for the badge record. 76 pub cid: String, 77 /// Human-readable badge name. 78 pub name: String, 79 /// Optional image reference (blob CID). 80 pub image: Option<String>, 81 /// Record creation timestamp. 82 pub created_at: DateTime<Utc>, 83 /// Record last update timestamp. 84 pub updated_at: DateTime<Utc>, 85 /// Number of awards using this badge. 86 pub count: i64, 87 /// Full JSON record of the badge definition. 88 pub record: Value, 89} 90 91/// Award with enriched badge and identity information. 92#[derive(Debug, Clone)] 93pub struct AwardWithBadge { 94 /// The award record. 95 pub award: Award, 96 /// Associated badge information if available. 97 pub badge: Option<Badge>, 98 /// Recipient identity information if available. 99 pub identity: Option<Identity>, 100 /// Identity information for award signers. 101 pub signer_identities: Vec<Identity>, 102} 103 104/// Storage trait defining the interface for badge storage operations 105#[async_trait] 106pub trait Storage: Send + Sync { 107 /// Run database migrations 108 async fn migrate(&self) -> Result<()>; 109 110 /// Insert or update an identity 111 async fn upsert_identity(&self, identity: &Identity) -> Result<()>; 112 113 /// Get an identity by DID 114 async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>>; 115 116 /// Get an identity by handle 117 async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>>; 118 119 /// Insert or update a badge 120 async fn upsert_badge(&self, badge: &Badge) -> Result<()>; 121 122 /// Get a badge by AT-URI and CID 123 async fn get_badge(&self, aturi: &str, cid: &str) -> Result<Option<Badge>>; 124 125 /// Increment the count for a badge 126 async fn increment_badge_count(&self, aturi: &str, cid: &str) -> Result<()>; 127 128 /// Decrement the count for a badge 129 async fn decrement_badge_count(&self, aturi: &str, cid: &str) -> Result<()>; 130 131 /// Insert or update an award, returns true if it's a new award 132 async fn upsert_award(&self, award: &Award) -> Result<bool>; 133 134 /// Get an award by AT-URI 135 async fn get_award(&self, aturi: &str) -> Result<Option<Award>>; 136 137 /// Delete an award by AT-URI 138 async fn delete_award(&self, aturi: &str) -> Result<Option<Award>>; 139 140 /// Trim awards for a DID to keep only the most recent ones 141 async fn trim_awards_for_did(&self, did: &str, max_count: i64) -> Result<()>; 142 143 /// Get recent awards with enriched badge and identity information 144 async fn get_recent_awards(&self, limit: i64) -> Result<Vec<AwardWithBadge>>; 145 146 /// Get awards for a specific DID with enriched information 147 async fn get_awards_for_did(&self, did: &str, limit: i64) -> Result<Vec<AwardWithBadge>>; 148} 149 150#[cfg(test)] 151mod tests { 152 use super::*; 153 use chrono::Utc; 154 use serde_json::json; 155 156 #[test] 157 fn test_badge_with_record() { 158 let record = json!({ 159 "name": "Test Badge", 160 "description": "A test badge for unit testing", 161 "image": { 162 "$type": "blob", 163 "ref": {"$link": "bafkreiabc123"}, 164 "mimeType": "image/png", 165 "size": 1024 166 } 167 }); 168 169 let badge = Badge { 170 aturi: "at://did:plc:test/community.lexicon.badge.definition/test".to_string(), 171 cid: "bafyreiabc123".to_string(), 172 name: "Test Badge".to_string(), 173 image: Some("bafkreiabc123".to_string()), 174 created_at: Utc::now(), 175 updated_at: Utc::now(), 176 count: 0, 177 record: record.clone(), 178 }; 179 180 // Verify the badge contains the record 181 assert_eq!(badge.record, record); 182 assert_eq!(badge.name, "Test Badge"); 183 assert_eq!(badge.record["name"], "Test Badge"); 184 assert_eq!(badge.record["description"], "A test badge for unit testing"); 185 } 186 187 #[test] 188 fn test_badge_record_serialization() { 189 let record = json!({ 190 "name": "Serialization Test", 191 "description": "Testing JSON serialization", 192 "issuer": "did:plc:issuer" 193 }); 194 195 let badge = Badge { 196 aturi: "at://did:plc:test/community.lexicon.badge.definition/serial".to_string(), 197 cid: "bafyreiserial".to_string(), 198 name: "Serialization Test".to_string(), 199 image: None, 200 created_at: Utc::now(), 201 updated_at: Utc::now(), 202 count: 5, 203 record: record.clone(), 204 }; 205 206 // Test that we can serialize and deserialize the badge 207 let serialized = serde_json::to_string(&badge).expect("Failed to serialize badge"); 208 let deserialized: Badge = 209 serde_json::from_str(&serialized).expect("Failed to deserialize badge"); 210 211 assert_eq!(deserialized.record, record); 212 assert_eq!(deserialized.name, badge.name); 213 assert_eq!(deserialized.count, badge.count); 214 } 215}