use crate::error::AprError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AprMetadata {
pub name: String,
pub version: semver::Version,
pub author: String,
pub license: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub difficulty_levels: Option<u8>,
#[serde(default)]
pub input_schema: Option<Schema>,
#[serde(default)]
pub output_schema: Option<Schema>,
#[serde(default)]
pub file_size: u64,
#[serde(default)]
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct Schema {
pub fields: Vec<SchemaField>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchemaField {
pub name: String,
pub field_type: String,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Default)]
pub struct AprMetadataBuilder {
name: Option<String>,
version: Option<String>,
author: Option<String>,
license: Option<String>,
description: Option<String>,
difficulty_levels: Option<u8>,
input_schema: Option<Schema>,
output_schema: Option<Schema>,
}
impl AprMetadataBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
#[must_use]
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
#[must_use]
pub fn license(mut self, license: impl Into<String>) -> Self {
self.license = Some(license.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub const fn difficulty_levels(mut self, levels: u8) -> Self {
self.difficulty_levels = Some(levels);
self
}
#[must_use]
pub fn input_schema(mut self, schema: Schema) -> Self {
self.input_schema = Some(schema);
self
}
#[must_use]
pub fn output_schema(mut self, schema: Schema) -> Self {
self.output_schema = Some(schema);
self
}
pub fn build(self) -> Result<AprMetadata, AprError> {
let name = self.name.ok_or(AprError::MissingField { field: "name" })?;
let version_str = self
.version
.ok_or(AprError::MissingField { field: "version" })?;
let author = self
.author
.ok_or(AprError::MissingField { field: "author" })?;
let license = self
.license
.ok_or(AprError::MissingField { field: "license" })?;
if name.len() < 3 || name.len() > 50 {
return Err(AprError::InvalidName { name });
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(AprError::InvalidName { name });
}
let version =
semver::Version::parse(&version_str).map_err(|_| AprError::InvalidVersion {
version: version_str,
})?;
Ok(AprMetadata {
name,
version,
author,
license,
description: self.description.unwrap_or_default(),
difficulty_levels: self.difficulty_levels,
input_schema: self.input_schema,
output_schema: self.output_schema,
file_size: 0,
created_at: Some(chrono::Utc::now()),
})
}
}
impl AprMetadata {
#[must_use]
pub fn builder() -> AprMetadataBuilder {
AprMetadataBuilder::new()
}
pub fn to_cbor(&self) -> Result<Vec<u8>, AprError> {
let mut buffer = Vec::new();
ciborium::into_writer(self, &mut buffer)
.map_err(|e| AprError::CborEncode(e.to_string()))?;
Ok(buffer)
}
pub fn from_cbor(bytes: &[u8]) -> Result<Self, AprError> {
ciborium::from_reader(bytes).map_err(|e| AprError::CborDecode(e.to_string()))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_builder_all_required() {
let result = AprMetadata::builder()
.name("test")
.version("1.0.0")
.author("Author")
.license("MIT")
.build();
assert!(result.is_ok());
}
#[test]
fn test_builder_missing_name() {
let result = AprMetadata::builder()
.version("1.0.0")
.author("Author")
.license("MIT")
.build();
assert!(matches!(
result,
Err(AprError::MissingField { field: "name" })
));
}
#[test]
fn test_name_too_short() {
let result = AprMetadata::builder()
.name("ab")
.version("1.0.0")
.author("Author")
.license("MIT")
.build();
assert!(matches!(result, Err(AprError::InvalidName { .. })));
}
#[test]
fn test_name_too_long() {
let long_name = "a".repeat(51);
let result = AprMetadata::builder()
.name(long_name)
.version("1.0.0")
.author("Author")
.license("MIT")
.build();
assert!(matches!(result, Err(AprError::InvalidName { .. })));
}
#[test]
fn test_name_invalid_chars() {
let result = AprMetadata::builder()
.name("test model!") .version("1.0.0")
.author("Author")
.license("MIT")
.build();
assert!(matches!(result, Err(AprError::InvalidName { .. })));
}
#[test]
fn test_invalid_version() {
let result = AprMetadata::builder()
.name("test")
.version("not.a.version")
.author("Author")
.license("MIT")
.build();
assert!(matches!(result, Err(AprError::InvalidVersion { .. })));
}
#[test]
fn test_cbor_roundtrip() {
let original = AprMetadata::builder()
.name("test-model")
.version("1.2.3")
.author("Test Author")
.license("MIT")
.description("A test description")
.difficulty_levels(5)
.build()
.expect("Should build");
let encoded = original.to_cbor().expect("Should encode");
let decoded = AprMetadata::from_cbor(&encoded).expect("Should decode");
assert_eq!(original.name, decoded.name);
assert_eq!(original.version, decoded.version);
assert_eq!(original.author, decoded.author);
assert_eq!(original.license, decoded.license);
assert_eq!(original.description, decoded.description);
assert_eq!(original.difficulty_levels, decoded.difficulty_levels);
}
}