1#[cfg(feature = "schema")]
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use super::core::*;
8use super::enums::{ContentCategory, ContentStatus};
9use super::validation::ValidationError;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[cfg_attr(feature = "schema", derive(JsonSchema))]
14pub struct ContentMetadata {
15 pub id: uuid::Uuid,
16 pub cid: ContentCid,
17 pub title: String,
18 pub description: String,
19 pub category: ContentCategory,
20 pub tags: Vec<String>,
21 pub size_bytes: Bytes,
22 pub chunk_count: u64,
23 pub price: Points,
24 pub creator_id: uuid::Uuid,
25 pub status: ContentStatus,
26 pub preview_images: Vec<String>,
27 pub created_at: chrono::DateTime<chrono::Utc>,
28 pub updated_at: chrono::DateTime<chrono::Utc>,
29}
30
31impl ContentMetadata {
32 pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
38 let mut errors = Vec::new();
39
40 if self.cid.is_empty() {
42 errors.push(ValidationError::EmptyCid);
43 }
44
45 if self.title.len() > MAX_TITLE_LENGTH {
47 errors.push(ValidationError::TitleTooLong {
48 length: self.title.len(),
49 max: MAX_TITLE_LENGTH,
50 });
51 }
52
53 if self.description.len() > MAX_DESCRIPTION_LENGTH {
55 errors.push(ValidationError::DescriptionTooLong {
56 length: self.description.len(),
57 max: MAX_DESCRIPTION_LENGTH,
58 });
59 }
60
61 if self.tags.len() > MAX_TAGS_COUNT {
63 errors.push(ValidationError::TooManyTags {
64 count: self.tags.len(),
65 max: MAX_TAGS_COUNT,
66 });
67 }
68 for tag in &self.tags {
69 if tag.len() > MAX_TAG_LENGTH {
70 errors.push(ValidationError::TagTooLong {
71 tag: tag.clone(),
72 length: tag.len(),
73 max: MAX_TAG_LENGTH,
74 });
75 }
76 }
77
78 if self.size_bytes < MIN_CONTENT_SIZE || self.size_bytes > MAX_CONTENT_SIZE {
80 errors.push(ValidationError::ContentSizeOutOfBounds {
81 size: self.size_bytes,
82 min: MIN_CONTENT_SIZE,
83 max: MAX_CONTENT_SIZE,
84 });
85 }
86
87 if errors.is_empty() {
88 Ok(())
89 } else {
90 Err(errors)
91 }
92 }
93
94 #[inline]
96 pub fn is_valid(&self) -> bool {
97 self.validate().is_ok()
98 }
99
100 #[inline]
102 pub fn expected_chunk_count(&self) -> u64 {
103 self.size_bytes.div_ceil(CHUNK_SIZE as u64)
104 }
105
106 #[inline]
108 pub fn size_mb(&self) -> f64 {
109 self.size_bytes as f64 / (1024.0 * 1024.0)
110 }
111
112 #[inline]
114 pub fn size_gb(&self) -> f64 {
115 self.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
116 }
117
118 #[inline]
120 pub fn is_active(&self) -> bool {
121 self.status == ContentStatus::Active
122 }
123
124 #[inline]
126 pub fn is_processing(&self) -> bool {
127 self.status == ContentStatus::Processing
128 }
129
130 #[inline]
132 pub fn is_unavailable(&self) -> bool {
133 matches!(
134 self.status,
135 ContentStatus::Removed | ContentStatus::Rejected
136 )
137 }
138}
139
140#[derive(Debug, Default)]
142pub struct ContentMetadataBuilder {
143 id: Option<uuid::Uuid>,
144 cid: Option<ContentCid>,
145 title: Option<String>,
146 description: String,
147 category: ContentCategory,
148 tags: Vec<String>,
149 size_bytes: Bytes,
150 chunk_count: Option<u64>,
151 price: Points,
152 creator_id: Option<uuid::Uuid>,
153 status: ContentStatus,
154 preview_images: Vec<String>,
155}
156
157impl ContentMetadataBuilder {
158 pub fn new() -> Self {
160 Self {
161 category: ContentCategory::Other,
162 status: ContentStatus::Processing,
163 ..Default::default()
164 }
165 }
166
167 pub fn id(mut self, id: uuid::Uuid) -> Self {
169 self.id = Some(id);
170 self
171 }
172
173 pub fn cid(mut self, cid: impl Into<String>) -> Self {
175 self.cid = Some(cid.into());
176 self
177 }
178
179 pub fn title(mut self, title: impl Into<String>) -> Self {
181 self.title = Some(title.into());
182 self
183 }
184
185 pub fn description(mut self, description: impl Into<String>) -> Self {
187 self.description = description.into();
188 self
189 }
190
191 pub fn category(mut self, category: ContentCategory) -> Self {
193 self.category = category;
194 self
195 }
196
197 pub fn tags(mut self, tags: Vec<String>) -> Self {
199 self.tags = tags;
200 self
201 }
202
203 pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
205 self.tags.push(tag.into());
206 self
207 }
208
209 pub fn size_bytes(mut self, size: Bytes) -> Self {
211 self.size_bytes = size;
212 self
213 }
214
215 pub fn chunk_count(mut self, count: u64) -> Self {
217 self.chunk_count = Some(count);
218 self
219 }
220
221 pub fn price(mut self, price: Points) -> Self {
223 self.price = price;
224 self
225 }
226
227 pub fn creator_id(mut self, creator_id: uuid::Uuid) -> Self {
229 self.creator_id = Some(creator_id);
230 self
231 }
232
233 pub fn status(mut self, status: ContentStatus) -> Self {
235 self.status = status;
236 self
237 }
238
239 pub fn preview_images(mut self, images: Vec<String>) -> Self {
241 self.preview_images = images;
242 self
243 }
244
245 pub fn build(self) -> Result<ContentMetadata, &'static str> {
251 let now = chrono::Utc::now();
252 let size_bytes = self.size_bytes;
253 let chunk_count = self
254 .chunk_count
255 .unwrap_or_else(|| size_bytes.div_ceil(CHUNK_SIZE as u64));
256
257 Ok(ContentMetadata {
258 id: self.id.unwrap_or_else(uuid::Uuid::new_v4),
259 cid: self.cid.ok_or("cid is required")?,
260 title: self.title.ok_or("title is required")?,
261 description: self.description,
262 category: self.category,
263 tags: self.tags,
264 size_bytes,
265 chunk_count,
266 price: self.price,
267 creator_id: self.creator_id.ok_or("creator_id is required")?,
268 status: self.status,
269 preview_images: self.preview_images,
270 created_at: now,
271 updated_at: now,
272 })
273 }
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[cfg_attr(feature = "schema", derive(JsonSchema))]
279pub struct ContentInvestment {
280 pub content_id: uuid::Uuid,
281 pub cid: ContentCid,
282 pub title: String,
283 pub current_seeders: u64,
284 pub demand_level: super::enums::DemandLevel,
285 pub predicted_revenue_per_gb: f64,
286 pub recommended_allocation_gb: f64,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291#[cfg_attr(feature = "schema", derive(JsonSchema))]
292pub struct ContentStats {
293 pub content_id: uuid::Uuid,
295 pub download_count: u64,
297 pub bandwidth_served: Bytes,
299 pub active_seeders: u64,
301 pub total_earnings: Points,
303 pub views: u64,
305 pub unique_downloaders: u64,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub average_rating: Option<f64>,
310 pub updated_at: chrono::DateTime<chrono::Utc>,
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_content_metadata_builder() {
320 let creator_id = uuid::Uuid::new_v4();
321 let metadata = ContentMetadataBuilder::new()
322 .cid("QmTestContent")
323 .title("Test Content")
324 .description("A test content item")
325 .category(ContentCategory::ThreeDModels)
326 .add_tag("blender")
327 .add_tag("lowpoly")
328 .size_bytes(1024 * 1024)
329 .price(100)
330 .creator_id(creator_id)
331 .build()
332 .unwrap();
333
334 assert_eq!(metadata.cid, "QmTestContent");
335 assert_eq!(metadata.title, "Test Content");
336 assert_eq!(metadata.category, ContentCategory::ThreeDModels);
337 assert_eq!(metadata.tags.len(), 2);
338 assert_eq!(metadata.chunk_count, metadata.expected_chunk_count());
339 assert!(metadata.is_valid());
340 }
341
342 #[test]
343 fn test_content_metadata_validation_title_too_long() {
344 let creator_id = uuid::Uuid::new_v4();
345 let long_title = "a".repeat(MAX_TITLE_LENGTH + 1);
346 let metadata = ContentMetadataBuilder::new()
347 .cid("QmTest")
348 .title(long_title)
349 .creator_id(creator_id)
350 .size_bytes(10000)
351 .build()
352 .unwrap();
353
354 let result = metadata.validate();
355 assert!(result.is_err());
356 let errors = result.unwrap_err();
357 assert!(
358 errors
359 .iter()
360 .any(|e| matches!(e, ValidationError::TitleTooLong { .. }))
361 );
362 }
363
364 #[test]
365 fn test_content_metadata_validation_too_many_tags() {
366 let creator_id = uuid::Uuid::new_v4();
367 let mut builder = ContentMetadataBuilder::new()
368 .cid("QmTest")
369 .title("Test")
370 .creator_id(creator_id)
371 .size_bytes(10000);
372
373 for i in 0..(MAX_TAGS_COUNT + 1) {
374 builder = builder.add_tag(format!("tag{}", i));
375 }
376
377 let metadata = builder.build().unwrap();
378 let result = metadata.validate();
379 assert!(result.is_err());
380 let errors = result.unwrap_err();
381 assert!(
382 errors
383 .iter()
384 .any(|e| matches!(e, ValidationError::TooManyTags { .. }))
385 );
386 }
387
388 #[test]
389 fn test_content_metadata_validation_size_out_of_bounds() {
390 let creator_id = uuid::Uuid::new_v4();
391
392 let metadata_small = ContentMetadataBuilder::new()
394 .cid("QmTest")
395 .title("Test")
396 .creator_id(creator_id)
397 .size_bytes(100) .build()
399 .unwrap();
400
401 let result = metadata_small.validate();
402 assert!(result.is_err());
403
404 let metadata_large = ContentMetadataBuilder::new()
406 .cid("QmTest")
407 .title("Test")
408 .creator_id(creator_id)
409 .size_bytes(MAX_CONTENT_SIZE + 1)
410 .build()
411 .unwrap();
412
413 let result = metadata_large.validate();
414 assert!(result.is_err());
415 }
416
417 #[test]
418 fn test_content_metadata_expected_chunk_count() {
419 let creator_id = uuid::Uuid::new_v4();
420 let size = CHUNK_SIZE as u64 * 5 + 1000; let metadata = ContentMetadataBuilder::new()
422 .cid("QmTest")
423 .title("Test")
424 .creator_id(creator_id)
425 .size_bytes(size)
426 .build()
427 .unwrap();
428
429 assert_eq!(metadata.expected_chunk_count(), 6);
430 }
431
432 #[test]
433 fn test_content_metadata_serialization() {
434 let metadata = ContentMetadataBuilder::new()
435 .cid("QmTest")
436 .title("Test")
437 .creator_id(uuid::Uuid::new_v4())
438 .size_bytes(10000)
439 .build()
440 .unwrap();
441
442 let json = serde_json::to_string(&metadata).unwrap();
443 let deserialized: ContentMetadata = serde_json::from_str(&json).unwrap();
444 assert_eq!(metadata.cid, deserialized.cid);
445 assert_eq!(metadata.title, deserialized.title);
446 }
447}