1use crate::{
8 BandwidthProof, ContentCategory, ContentMetadata, ContentStatus, DemandLevel, NodeStats,
9 NodeStatus, UserRole,
10};
11
12pub trait FromDbModel<T> {
14 fn from_db_model(model: T) -> Self;
16}
17
18pub trait ToDbModel<T> {
20 fn to_db_model(&self) -> T;
22}
23
24pub trait DbModelConvert<T>: FromDbModel<T> + ToDbModel<T> {}
26
27impl<S, T> DbModelConvert<T> for S where S: FromDbModel<T> + ToDbModel<T> {}
29
30#[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, price: metadata.price,
58 }
59 }
60}
61
62#[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#[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 #[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 #[must_use]
129 pub const fn with_role(mut self, role: UserRole) -> Self {
130 self.role = role;
131 self
132 }
133
134 #[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#[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 #[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(), peer_id: stats.peer_id.clone(),
177 public_key: Vec::new(), max_storage_bytes: stats.pinned_storage_bytes,
179 max_bandwidth_bps: 0, }
181 }
182}
183
184#[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 #[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 #[must_use]
207 pub fn has_more(&self) -> bool {
208 self.offset + (self.items.len() as u64) < self.total_count
209 }
210
211 #[must_use]
213 pub fn next_offset(&self) -> u64 {
214 self.offset + self.items.len() as u64
215 }
216}
217
218#[derive(Debug, Clone, Default)]
220pub struct ContentFilter {
221 pub creator_id: Option<uuid::Uuid>,
223 pub category: Option<ContentCategory>,
225 pub status: Option<ContentStatus>,
227 pub min_price: Option<u64>,
229 pub max_price: Option<u64>,
231 pub search: Option<String>,
233 pub tags: Option<Vec<String>>,
235 pub order_by: Option<ContentOrderBy>,
237 pub order_desc: bool,
239}
240
241impl ContentFilter {
242 #[must_use]
244 pub fn new() -> Self {
245 Self::default()
246 }
247
248 #[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 #[must_use]
257 pub fn category(mut self, category: ContentCategory) -> Self {
258 self.category = Some(category);
259 self
260 }
261
262 #[must_use]
264 pub fn status(mut self, status: ContentStatus) -> Self {
265 self.status = Some(status);
266 self
267 }
268
269 #[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 #[must_use]
279 pub fn search(mut self, query: impl Into<String>) -> Self {
280 self.search = Some(query.into());
281 self
282 }
283
284 #[must_use]
286 pub fn tags(mut self, tags: Vec<String>) -> Self {
287 self.tags = Some(tags);
288 self
289 }
290
291 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum ContentOrderBy {
303 CreatedAt,
305 UpdatedAt,
307 Price,
309 DownloadCount,
311 Title,
313 Size,
315}
316
317#[derive(Debug, Clone, Default)]
319pub struct NodeFilter {
320 pub user_id: Option<uuid::Uuid>,
322 pub status: Option<NodeStatus>,
324 pub min_reputation: Option<f32>,
326 pub region: Option<String>,
328}
329
330impl NodeFilter {
331 #[must_use]
333 pub fn new() -> Self {
334 Self::default()
335 }
336
337 #[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 #[must_use]
346 pub fn status(mut self, status: NodeStatus) -> Self {
347 self.status = Some(status);
348 self
349 }
350
351 #[must_use]
353 pub fn min_reputation(mut self, score: f32) -> Self {
354 self.min_reputation = Some(score);
355 self
356 }
357
358 #[must_use]
360 pub fn region(mut self, region: impl Into<String>) -> Self {
361 self.region = Some(region.into());
362 self
363 }
364}
365
366pub trait ToSqlEnum {
368 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
431pub trait FromSqlEnum: Sized {
433 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}