1use std::{collections::HashMap, sync::Arc};
7
8use arrow::record_batch::RecordBatch;
9use serde::{Deserialize, Serialize};
10
11use crate::{error::Result, serve::schema::ContentSchema};
12
13#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
22pub struct ContentTypeId(String);
23
24impl ContentTypeId {
25 pub const DATASET: &'static str = "alimentar.dataset";
27 pub const COURSE: &'static str = "assetgen.course";
29 pub const MODEL: &'static str = "aprender.model";
31 pub const REGISTRY: &'static str = "alimentar.registry";
33 pub const RAW: &'static str = "alimentar.raw";
35
36 pub fn new(id: impl Into<String>) -> Self {
38 Self(id.into())
39 }
40
41 pub fn dataset() -> Self {
43 Self(Self::DATASET.to_string())
44 }
45
46 pub fn course() -> Self {
48 Self(Self::COURSE.to_string())
49 }
50
51 pub fn model() -> Self {
53 Self(Self::MODEL.to_string())
54 }
55
56 pub fn registry() -> Self {
58 Self(Self::REGISTRY.to_string())
59 }
60
61 pub fn raw() -> Self {
63 Self(Self::RAW.to_string())
64 }
65
66 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70
71 pub fn is_builtin(&self) -> bool {
73 matches!(
74 self.0.as_str(),
75 Self::DATASET | Self::COURSE | Self::MODEL | Self::REGISTRY | Self::RAW
76 )
77 }
78}
79
80impl std::fmt::Display for ContentTypeId {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "{}", self.0)
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ContentMetadata {
89 pub content_type: ContentTypeId,
91 pub title: String,
93 pub description: Option<String>,
95 pub size: usize,
97 pub row_count: Option<usize>,
99 pub schema: Option<ContentSchema>,
101 pub source: Option<String>,
103 #[serde(default)]
105 pub custom: HashMap<String, serde_json::Value>,
106}
107
108impl ContentMetadata {
109 pub fn new(content_type: ContentTypeId, title: impl Into<String>, size: usize) -> Self {
111 Self {
112 content_type,
113 title: title.into(),
114 description: None,
115 size,
116 row_count: None,
117 schema: None,
118 source: None,
119 custom: HashMap::new(),
120 }
121 }
122
123 pub fn with_description(mut self, description: impl Into<String>) -> Self {
125 self.description = Some(description.into());
126 self
127 }
128
129 pub fn with_row_count(mut self, count: usize) -> Self {
131 self.row_count = Some(count);
132 self
133 }
134
135 pub fn with_schema(mut self, schema: ContentSchema) -> Self {
137 self.schema = Some(schema);
138 self
139 }
140
141 pub fn with_source(mut self, source: impl Into<String>) -> Self {
143 self.source = Some(source.into());
144 self
145 }
146
147 pub fn with_custom(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
149 self.custom.insert(key.into(), value);
150 self
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ValidationReport {
157 pub valid: bool,
159 pub errors: Vec<ValidationError>,
161 pub warnings: Vec<ValidationWarning>,
163}
164
165impl ValidationReport {
166 pub fn success() -> Self {
168 Self {
169 valid: true,
170 errors: Vec::new(),
171 warnings: Vec::new(),
172 }
173 }
174
175 pub fn failure(errors: Vec<ValidationError>) -> Self {
177 Self {
178 valid: false,
179 errors,
180 warnings: Vec::new(),
181 }
182 }
183
184 pub fn with_warning(mut self, warning: ValidationWarning) -> Self {
186 self.warnings.push(warning);
187 self
188 }
189
190 pub fn with_error(mut self, error: ValidationError) -> Self {
192 self.valid = false;
193 self.errors.push(error);
194 self
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct ValidationError {
201 pub path: String,
203 pub message: String,
205 pub code: Option<String>,
207}
208
209impl ValidationError {
210 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
212 Self {
213 path: path.into(),
214 message: message.into(),
215 code: None,
216 }
217 }
218
219 pub fn with_code(mut self, code: impl Into<String>) -> Self {
221 self.code = Some(code.into());
222 self
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ValidationWarning {
229 pub path: String,
231 pub message: String,
233}
234
235impl ValidationWarning {
236 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
238 Self {
239 path: path.into(),
240 message: message.into(),
241 }
242 }
243}
244
245pub trait ServeableContent: Send + Sync {
250 fn schema(&self) -> ContentSchema;
252
253 fn validate(&self) -> Result<ValidationReport>;
259
260 fn to_arrow(&self) -> Result<RecordBatch>;
266
267 fn metadata(&self) -> ContentMetadata;
269
270 fn content_type(&self) -> ContentTypeId;
272
273 fn chunks(&self, chunk_size: usize) -> Box<dyn Iterator<Item = Result<RecordBatch>> + Send>;
275
276 fn to_bytes(&self) -> Result<Vec<u8>>;
282}
283
284pub type BoxedContent = Box<dyn ServeableContent>;
286
287#[allow(dead_code)]
289pub type SharedContent = Arc<dyn ServeableContent>;
290
291#[cfg(test)]
292#[allow(clippy::unwrap_used)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_content_type_id_new() {
298 let id = ContentTypeId::new("custom.type");
299 assert_eq!(id.as_str(), "custom.type");
300 }
301
302 #[test]
303 fn test_content_type_is_builtin() {
304 assert!(ContentTypeId::dataset().is_builtin());
305 assert!(ContentTypeId::course().is_builtin());
306 assert!(ContentTypeId::raw().is_builtin());
307 assert!(!ContentTypeId::new("custom.type").is_builtin());
308 }
309
310 #[test]
311 fn test_content_metadata_builder() {
312 let meta = ContentMetadata::new(ContentTypeId::dataset(), "Test Dataset", 1024)
313 .with_description("A test dataset")
314 .with_row_count(100)
315 .with_source("clipboard")
316 .with_custom("version", serde_json::json!("1.0"));
317
318 assert_eq!(meta.title, "Test Dataset");
319 assert_eq!(meta.description, Some("A test dataset".to_string()));
320 assert_eq!(meta.row_count, Some(100));
321 assert_eq!(meta.source, Some("clipboard".to_string()));
322 assert!(meta.custom.contains_key("version"));
323 }
324
325 #[test]
326 fn test_validation_report() {
327 let report = ValidationReport::success()
328 .with_warning(ValidationWarning::new("field1", "Optional field missing"));
329
330 assert!(report.valid);
331 assert!(report.errors.is_empty());
332 assert_eq!(report.warnings.len(), 1);
333
334 let report = ValidationReport::failure(vec![ValidationError::new(
335 "field2",
336 "Required field missing",
337 )
338 .with_code("REQUIRED_FIELD")]);
339
340 assert!(!report.valid);
341 assert_eq!(report.errors.len(), 1);
342 assert_eq!(report.errors[0].code, Some("REQUIRED_FIELD".to_string()));
343 }
344
345 #[test]
346 fn test_validation_report_with_error() {
347 let report = ValidationReport::success().with_error(ValidationError::new("field", "Error"));
348
349 assert!(!report.valid);
350 assert_eq!(report.errors.len(), 1);
351 }
352
353 #[test]
354 fn test_content_type_id_model() {
355 let model = ContentTypeId::model();
356 assert_eq!(model.as_str(), "aprender.model");
357 assert!(model.is_builtin());
358 }
359
360 #[test]
361 fn test_content_type_id_registry() {
362 let registry = ContentTypeId::registry();
363 assert_eq!(registry.as_str(), "alimentar.registry");
364 assert!(registry.is_builtin());
365 }
366
367 #[test]
368 fn test_validation_error_without_code() {
369 let err = ValidationError::new("path", "message");
370 assert!(err.code.is_none());
371 assert_eq!(err.path, "path");
372 assert_eq!(err.message, "message");
373 }
374}