use std::{collections::HashMap, sync::Arc};
use arrow::record_batch::RecordBatch;
use serde::{Deserialize, Serialize};
use crate::{error::Result, serve::schema::ContentSchema};
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct ContentTypeId(String);
impl ContentTypeId {
pub const DATASET: &'static str = "alimentar.dataset";
pub const COURSE: &'static str = "assetgen.course";
pub const MODEL: &'static str = "aprender.model";
pub const REGISTRY: &'static str = "alimentar.registry";
pub const RAW: &'static str = "alimentar.raw";
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn dataset() -> Self {
Self(Self::DATASET.to_string())
}
pub fn course() -> Self {
Self(Self::COURSE.to_string())
}
pub fn model() -> Self {
Self(Self::MODEL.to_string())
}
pub fn registry() -> Self {
Self(Self::REGISTRY.to_string())
}
pub fn raw() -> Self {
Self(Self::RAW.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_builtin(&self) -> bool {
matches!(
self.0.as_str(),
Self::DATASET | Self::COURSE | Self::MODEL | Self::REGISTRY | Self::RAW
)
}
}
impl std::fmt::Display for ContentTypeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentMetadata {
pub content_type: ContentTypeId,
pub title: String,
pub description: Option<String>,
pub size: usize,
pub row_count: Option<usize>,
pub schema: Option<ContentSchema>,
pub source: Option<String>,
#[serde(default)]
pub custom: HashMap<String, serde_json::Value>,
}
impl ContentMetadata {
pub fn new(content_type: ContentTypeId, title: impl Into<String>, size: usize) -> Self {
Self {
content_type,
title: title.into(),
description: None,
size,
row_count: None,
schema: None,
source: None,
custom: HashMap::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_row_count(mut self, count: usize) -> Self {
self.row_count = Some(count);
self
}
pub fn with_schema(mut self, schema: ContentSchema) -> Self {
self.schema = Some(schema);
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub fn with_custom(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.custom.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationReport {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
}
impl ValidationReport {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn failure(errors: Vec<ValidationError>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
}
}
pub fn with_warning(mut self, warning: ValidationWarning) -> Self {
self.warnings.push(warning);
self
}
pub fn with_error(mut self, error: ValidationError) -> Self {
self.valid = false;
self.errors.push(error);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub path: String,
pub message: String,
pub code: Option<String>,
}
impl ValidationError {
pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
Self {
path: path.into(),
message: message.into(),
code: None,
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWarning {
pub path: String,
pub message: String,
}
impl ValidationWarning {
pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
Self {
path: path.into(),
message: message.into(),
}
}
}
pub trait ServeableContent: Send + Sync {
fn schema(&self) -> ContentSchema;
fn validate(&self) -> Result<ValidationReport>;
fn to_arrow(&self) -> Result<RecordBatch>;
fn metadata(&self) -> ContentMetadata;
fn content_type(&self) -> ContentTypeId;
fn chunks(&self, chunk_size: usize) -> Box<dyn Iterator<Item = Result<RecordBatch>> + Send>;
fn to_bytes(&self) -> Result<Vec<u8>>;
}
pub type BoxedContent = Box<dyn ServeableContent>;
#[allow(dead_code)]
pub type SharedContent = Arc<dyn ServeableContent>;
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_content_type_id_new() {
let id = ContentTypeId::new("custom.type");
assert_eq!(id.as_str(), "custom.type");
}
#[test]
fn test_content_type_is_builtin() {
assert!(ContentTypeId::dataset().is_builtin());
assert!(ContentTypeId::course().is_builtin());
assert!(ContentTypeId::raw().is_builtin());
assert!(!ContentTypeId::new("custom.type").is_builtin());
}
#[test]
fn test_content_metadata_builder() {
let meta = ContentMetadata::new(ContentTypeId::dataset(), "Test Dataset", 1024)
.with_description("A test dataset")
.with_row_count(100)
.with_source("clipboard")
.with_custom("version", serde_json::json!("1.0"));
assert_eq!(meta.title, "Test Dataset");
assert_eq!(meta.description, Some("A test dataset".to_string()));
assert_eq!(meta.row_count, Some(100));
assert_eq!(meta.source, Some("clipboard".to_string()));
assert!(meta.custom.contains_key("version"));
}
#[test]
fn test_validation_report() {
let report = ValidationReport::success()
.with_warning(ValidationWarning::new("field1", "Optional field missing"));
assert!(report.valid);
assert!(report.errors.is_empty());
assert_eq!(report.warnings.len(), 1);
let report = ValidationReport::failure(vec![ValidationError::new(
"field2",
"Required field missing",
)
.with_code("REQUIRED_FIELD")]);
assert!(!report.valid);
assert_eq!(report.errors.len(), 1);
assert_eq!(report.errors[0].code, Some("REQUIRED_FIELD".to_string()));
}
#[test]
fn test_validation_report_with_error() {
let report = ValidationReport::success().with_error(ValidationError::new("field", "Error"));
assert!(!report.valid);
assert_eq!(report.errors.len(), 1);
}
#[test]
fn test_content_type_id_model() {
let model = ContentTypeId::model();
assert_eq!(model.as_str(), "aprender.model");
assert!(model.is_builtin());
}
#[test]
fn test_content_type_id_registry() {
let registry = ContentTypeId::registry();
assert_eq!(registry.as_str(), "alimentar.registry");
assert!(registry.is_builtin());
}
#[test]
fn test_validation_error_without_code() {
let err = ValidationError::new("path", "message");
assert!(err.code.is_none());
assert_eq!(err.path, "path");
assert_eq!(err.message, "message");
}
}