chie_shared/
conversions.rs

1//! Conversion traits between shared types and database models.
2//!
3//! This module provides traits for converting between the shared protocol
4//! types and database-specific models. The implementations live in chie-coordinator
5//! to avoid circular dependencies.
6
7use crate::{
8    BandwidthProof, ContentCategory, ContentMetadata, ContentStatus, DemandLevel, NodeStats,
9    NodeStatus, UserRole,
10};
11
12/// Trait for converting from a database model to a shared type.
13pub trait FromDbModel<T> {
14    /// Convert from the database model.
15    fn from_db_model(model: T) -> Self;
16}
17
18/// Trait for converting to a database model from a shared type.
19pub trait ToDbModel<T> {
20    /// Convert to the database model.
21    fn to_db_model(&self) -> T;
22}
23
24/// Trait for types that can be converted bidirectionally with database models.
25pub trait DbModelConvert<T>: FromDbModel<T> + ToDbModel<T> {}
26
27// Blanket implementation
28impl<S, T> DbModelConvert<T> for S where S: FromDbModel<T> + ToDbModel<T> {}
29
30/// Input type for creating content from shared type.
31#[derive(Debug, Clone)]
32pub struct CreateContentInput {
33    pub creator_id: uuid::Uuid,
34    pub title: String,
35    pub description: Option<String>,
36    pub category: ContentCategory,
37    pub tags: Vec<String>,
38    pub cid: String,
39    pub size_bytes: u64,
40    pub chunk_count: u64,
41    pub encryption_key: Option<Vec<u8>>,
42    pub price: u64,
43}
44
45impl From<&ContentMetadata> for CreateContentInput {
46    fn from(metadata: &ContentMetadata) -> Self {
47        Self {
48            creator_id: metadata.creator_id,
49            title: metadata.title.clone(),
50            description: Some(metadata.description.clone()),
51            category: metadata.category,
52            tags: metadata.tags.clone(),
53            cid: metadata.cid.clone(),
54            size_bytes: metadata.size_bytes,
55            chunk_count: metadata.chunk_count,
56            encryption_key: None, // Encryption key is stored separately
57            price: metadata.price,
58        }
59    }
60}
61
62/// Input type for creating a bandwidth proof record.
63#[derive(Debug, Clone)]
64pub struct CreateProofInput {
65    pub session_id: uuid::Uuid,
66    pub content_cid: String,
67    pub chunk_index: u64,
68    pub bytes_transferred: u64,
69    pub provider_peer_id: String,
70    pub requester_peer_id: String,
71    pub provider_public_key: Vec<u8>,
72    pub requester_public_key: Vec<u8>,
73    pub provider_signature: Vec<u8>,
74    pub requester_signature: Vec<u8>,
75    pub challenge_nonce: Vec<u8>,
76    pub chunk_hash: Vec<u8>,
77    pub start_timestamp_ms: i64,
78    pub end_timestamp_ms: i64,
79    pub latency_ms: u32,
80}
81
82impl From<&BandwidthProof> for CreateProofInput {
83    fn from(proof: &BandwidthProof) -> Self {
84        Self {
85            session_id: proof.session_id,
86            content_cid: proof.content_cid.clone(),
87            chunk_index: proof.chunk_index,
88            bytes_transferred: proof.bytes_transferred,
89            provider_peer_id: proof.provider_peer_id.clone(),
90            requester_peer_id: proof.requester_peer_id.clone(),
91            provider_public_key: proof.provider_public_key.clone(),
92            requester_public_key: proof.requester_public_key.clone(),
93            provider_signature: proof.provider_signature.clone(),
94            requester_signature: proof.requester_signature.clone(),
95            challenge_nonce: proof.challenge_nonce.clone(),
96            chunk_hash: proof.chunk_hash.clone(),
97            start_timestamp_ms: proof.start_timestamp_ms,
98            end_timestamp_ms: proof.end_timestamp_ms,
99            latency_ms: proof.latency_ms,
100        }
101    }
102}
103
104/// Input type for creating a user.
105#[derive(Debug, Clone)]
106pub struct CreateUserInput {
107    pub username: String,
108    pub email: String,
109    pub password_hash: String,
110    pub role: UserRole,
111    pub referrer_id: Option<uuid::Uuid>,
112}
113
114impl CreateUserInput {
115    /// Create a new user input for registration.
116    #[must_use]
117    pub fn new(username: String, email: String, password_hash: String) -> Self {
118        Self {
119            username,
120            email,
121            password_hash,
122            role: UserRole::User,
123            referrer_id: None,
124        }
125    }
126
127    /// Set the user role.
128    #[must_use]
129    pub const fn with_role(mut self, role: UserRole) -> Self {
130        self.role = role;
131        self
132    }
133
134    /// Set the referrer.
135    #[must_use]
136    pub fn with_referrer(mut self, referrer_id: uuid::Uuid) -> Self {
137        self.referrer_id = Some(referrer_id);
138        self
139    }
140}
141
142/// Input type for creating a node.
143#[derive(Debug, Clone)]
144pub struct CreateNodeInput {
145    pub user_id: uuid::Uuid,
146    pub peer_id: String,
147    pub public_key: Vec<u8>,
148    pub max_storage_bytes: u64,
149    pub max_bandwidth_bps: u64,
150}
151
152impl CreateNodeInput {
153    /// Create a new node input.
154    #[must_use]
155    pub fn new(
156        user_id: uuid::Uuid,
157        peer_id: String,
158        public_key: Vec<u8>,
159        max_storage_bytes: u64,
160        max_bandwidth_bps: u64,
161    ) -> Self {
162        Self {
163            user_id,
164            peer_id,
165            public_key,
166            max_storage_bytes,
167            max_bandwidth_bps,
168        }
169    }
170}
171
172impl From<&NodeStats> for CreateNodeInput {
173    fn from(stats: &NodeStats) -> Self {
174        Self {
175            user_id: uuid::Uuid::nil(), // Must be set separately
176            peer_id: stats.peer_id.clone(),
177            public_key: Vec::new(), // Must be set separately
178            max_storage_bytes: stats.pinned_storage_bytes,
179            max_bandwidth_bps: 0, // Default
180        }
181    }
182}
183
184/// Result of a database query for content list.
185#[derive(Debug, Clone)]
186pub struct ContentListResult {
187    pub items: Vec<ContentMetadata>,
188    pub total_count: u64,
189    pub offset: u64,
190    pub limit: u64,
191}
192
193impl ContentListResult {
194    /// Create an empty result.
195    #[must_use]
196    pub fn empty() -> Self {
197        Self {
198            items: Vec::new(),
199            total_count: 0,
200            offset: 0,
201            limit: 0,
202        }
203    }
204
205    /// Check if there are more results.
206    #[must_use]
207    pub fn has_more(&self) -> bool {
208        self.offset + (self.items.len() as u64) < self.total_count
209    }
210
211    /// Get the next offset.
212    #[must_use]
213    pub fn next_offset(&self) -> u64 {
214        self.offset + self.items.len() as u64
215    }
216}
217
218/// Filter for content queries.
219#[derive(Debug, Clone, Default)]
220pub struct ContentFilter {
221    /// Filter by creator ID.
222    pub creator_id: Option<uuid::Uuid>,
223    /// Filter by category.
224    pub category: Option<ContentCategory>,
225    /// Filter by status.
226    pub status: Option<ContentStatus>,
227    /// Filter by minimum price.
228    pub min_price: Option<u64>,
229    /// Filter by maximum price.
230    pub max_price: Option<u64>,
231    /// Search in title/description.
232    pub search: Option<String>,
233    /// Filter by tags (any match).
234    pub tags: Option<Vec<String>>,
235    /// Order by field.
236    pub order_by: Option<ContentOrderBy>,
237    /// Order direction.
238    pub order_desc: bool,
239}
240
241impl ContentFilter {
242    /// Create a new empty filter.
243    #[must_use]
244    pub fn new() -> Self {
245        Self::default()
246    }
247
248    /// Filter by creator.
249    #[must_use]
250    pub fn creator(mut self, creator_id: uuid::Uuid) -> Self {
251        self.creator_id = Some(creator_id);
252        self
253    }
254
255    /// Filter by category.
256    #[must_use]
257    pub fn category(mut self, category: ContentCategory) -> Self {
258        self.category = Some(category);
259        self
260    }
261
262    /// Filter by status.
263    #[must_use]
264    pub fn status(mut self, status: ContentStatus) -> Self {
265        self.status = Some(status);
266        self
267    }
268
269    /// Filter by price range.
270    #[must_use]
271    pub fn price_range(mut self, min: Option<u64>, max: Option<u64>) -> Self {
272        self.min_price = min;
273        self.max_price = max;
274        self
275    }
276
277    /// Search in title and description.
278    #[must_use]
279    pub fn search(mut self, query: impl Into<String>) -> Self {
280        self.search = Some(query.into());
281        self
282    }
283
284    /// Filter by tags.
285    #[must_use]
286    pub fn tags(mut self, tags: Vec<String>) -> Self {
287        self.tags = Some(tags);
288        self
289    }
290
291    /// Order by field.
292    #[must_use]
293    pub fn order_by(mut self, field: ContentOrderBy, desc: bool) -> Self {
294        self.order_by = Some(field);
295        self.order_desc = desc;
296        self
297    }
298}
299
300/// Content ordering options.
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum ContentOrderBy {
303    /// Order by creation date.
304    CreatedAt,
305    /// Order by update date.
306    UpdatedAt,
307    /// Order by price.
308    Price,
309    /// Order by download count.
310    DownloadCount,
311    /// Order by title.
312    Title,
313    /// Order by size.
314    Size,
315}
316
317/// Filter for node queries.
318#[derive(Debug, Clone, Default)]
319pub struct NodeFilter {
320    /// Filter by user ID.
321    pub user_id: Option<uuid::Uuid>,
322    /// Filter by status.
323    pub status: Option<NodeStatus>,
324    /// Filter by minimum reputation.
325    pub min_reputation: Option<f32>,
326    /// Filter by region.
327    pub region: Option<String>,
328}
329
330impl NodeFilter {
331    /// Create a new empty filter.
332    #[must_use]
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    /// Filter by user.
338    #[must_use]
339    pub fn user(mut self, user_id: uuid::Uuid) -> Self {
340        self.user_id = Some(user_id);
341        self
342    }
343
344    /// Filter by status.
345    #[must_use]
346    pub fn status(mut self, status: NodeStatus) -> Self {
347        self.status = Some(status);
348        self
349    }
350
351    /// Filter by minimum reputation.
352    #[must_use]
353    pub fn min_reputation(mut self, score: f32) -> Self {
354        self.min_reputation = Some(score);
355        self
356    }
357
358    /// Filter by region.
359    #[must_use]
360    pub fn region(mut self, region: impl Into<String>) -> Self {
361        self.region = Some(region.into());
362        self
363    }
364}
365
366/// Trait for types that can be converted to SQL enum string.
367pub trait ToSqlEnum {
368    /// Get the SQL enum value as a string.
369    fn to_sql_enum(&self) -> &'static str;
370}
371
372impl ToSqlEnum for ContentCategory {
373    fn to_sql_enum(&self) -> &'static str {
374        match self {
375            Self::ThreeDModels => "THREE_D_MODELS",
376            Self::Textures => "TEXTURES",
377            Self::Audio => "AUDIO",
378            Self::Scripts => "SCRIPTS",
379            Self::Animations => "ANIMATIONS",
380            Self::AssetPacks => "ASSET_PACKS",
381            Self::AiModels => "AI_MODELS",
382            Self::Other => "OTHER",
383        }
384    }
385}
386
387impl ToSqlEnum for ContentStatus {
388    fn to_sql_enum(&self) -> &'static str {
389        match self {
390            Self::Processing => "PROCESSING",
391            Self::Active => "ACTIVE",
392            Self::PendingReview => "PENDING_REVIEW",
393            Self::Rejected => "REJECTED",
394            Self::Removed => "REMOVED",
395        }
396    }
397}
398
399impl ToSqlEnum for NodeStatus {
400    fn to_sql_enum(&self) -> &'static str {
401        match self {
402            Self::Online => "ONLINE",
403            Self::Offline => "OFFLINE",
404            Self::Syncing => "SYNCING",
405            Self::Banned => "BANNED",
406        }
407    }
408}
409
410impl ToSqlEnum for UserRole {
411    fn to_sql_enum(&self) -> &'static str {
412        match self {
413            Self::User => "USER",
414            Self::Creator => "CREATOR",
415            Self::Admin => "ADMIN",
416        }
417    }
418}
419
420impl ToSqlEnum for DemandLevel {
421    fn to_sql_enum(&self) -> &'static str {
422        match self {
423            Self::Low => "LOW",
424            Self::Medium => "MEDIUM",
425            Self::High => "HIGH",
426            Self::VeryHigh => "VERY_HIGH",
427        }
428    }
429}
430
431/// Trait for parsing from SQL enum string.
432pub trait FromSqlEnum: Sized {
433    /// Parse from SQL enum value.
434    fn from_sql_enum(s: &str) -> Option<Self>;
435}
436
437impl FromSqlEnum for ContentCategory {
438    fn from_sql_enum(s: &str) -> Option<Self> {
439        match s {
440            "THREE_D_MODELS" => Some(Self::ThreeDModels),
441            "TEXTURES" => Some(Self::Textures),
442            "AUDIO" => Some(Self::Audio),
443            "SCRIPTS" => Some(Self::Scripts),
444            "ANIMATIONS" => Some(Self::Animations),
445            "ASSET_PACKS" => Some(Self::AssetPacks),
446            "AI_MODELS" => Some(Self::AiModels),
447            "OTHER" => Some(Self::Other),
448            _ => None,
449        }
450    }
451}
452
453impl FromSqlEnum for ContentStatus {
454    fn from_sql_enum(s: &str) -> Option<Self> {
455        match s {
456            "PROCESSING" => Some(Self::Processing),
457            "ACTIVE" => Some(Self::Active),
458            "PENDING_REVIEW" => Some(Self::PendingReview),
459            "REJECTED" => Some(Self::Rejected),
460            "REMOVED" => Some(Self::Removed),
461            _ => None,
462        }
463    }
464}
465
466impl FromSqlEnum for NodeStatus {
467    fn from_sql_enum(s: &str) -> Option<Self> {
468        match s {
469            "ONLINE" => Some(Self::Online),
470            "OFFLINE" => Some(Self::Offline),
471            "SYNCING" => Some(Self::Syncing),
472            "BANNED" => Some(Self::Banned),
473            _ => None,
474        }
475    }
476}
477
478impl FromSqlEnum for UserRole {
479    fn from_sql_enum(s: &str) -> Option<Self> {
480        match s {
481            "USER" => Some(Self::User),
482            "CREATOR" => Some(Self::Creator),
483            "ADMIN" => Some(Self::Admin),
484            _ => None,
485        }
486    }
487}
488
489impl FromSqlEnum for DemandLevel {
490    fn from_sql_enum(s: &str) -> Option<Self> {
491        match s {
492            "LOW" => Some(Self::Low),
493            "MEDIUM" => Some(Self::Medium),
494            "HIGH" => Some(Self::High),
495            "VERY_HIGH" => Some(Self::VeryHigh),
496            _ => None,
497        }
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn test_content_category_sql_enum() {
507        assert_eq!(
508            ContentCategory::ThreeDModels.to_sql_enum(),
509            "THREE_D_MODELS"
510        );
511        assert_eq!(
512            ContentCategory::from_sql_enum("THREE_D_MODELS"),
513            Some(ContentCategory::ThreeDModels)
514        );
515        assert_eq!(ContentCategory::from_sql_enum("INVALID"), None);
516    }
517
518    #[test]
519    fn test_content_filter_builder() {
520        let filter = ContentFilter::new()
521            .category(ContentCategory::Audio)
522            .status(ContentStatus::Active)
523            .price_range(Some(100), Some(1000))
524            .order_by(ContentOrderBy::Price, true);
525
526        assert_eq!(filter.category, Some(ContentCategory::Audio));
527        assert_eq!(filter.status, Some(ContentStatus::Active));
528        assert_eq!(filter.min_price, Some(100));
529        assert_eq!(filter.max_price, Some(1000));
530        assert_eq!(filter.order_by, Some(ContentOrderBy::Price));
531        assert!(filter.order_desc);
532    }
533
534    #[test]
535    fn test_content_list_result() {
536        let result = ContentListResult {
537            items: vec![],
538            total_count: 100,
539            offset: 0,
540            limit: 10,
541        };
542
543        assert!(result.has_more());
544        assert_eq!(result.next_offset(), 0);
545
546        let empty = ContentListResult::empty();
547        assert!(!empty.has_more());
548    }
549}