1use crate::error::AprError;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct AprMetadata {
12 pub name: String,
14
15 pub version: semver::Version,
17
18 pub author: String,
20
21 pub license: String,
23
24 #[serde(default)]
26 pub description: String,
27
28 #[serde(default)]
30 pub difficulty_levels: Option<u8>,
31
32 #[serde(default)]
34 pub input_schema: Option<Schema>,
35
36 #[serde(default)]
38 pub output_schema: Option<Schema>,
39
40 #[serde(default)]
42 pub file_size: u64,
43
44 #[serde(default)]
46 pub created_at: Option<chrono::DateTime<chrono::Utc>>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
51pub struct Schema {
52 pub fields: Vec<SchemaField>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58pub struct SchemaField {
59 pub name: String,
61 pub field_type: String,
63 #[serde(default)]
65 pub description: String,
66}
67
68#[derive(Debug, Default)]
70pub struct AprMetadataBuilder {
71 name: Option<String>,
72 version: Option<String>,
73 author: Option<String>,
74 license: Option<String>,
75 description: Option<String>,
76 difficulty_levels: Option<u8>,
77 input_schema: Option<Schema>,
78 output_schema: Option<Schema>,
79}
80
81impl AprMetadataBuilder {
82 #[must_use]
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 #[must_use]
90 pub fn name(mut self, name: impl Into<String>) -> Self {
91 self.name = Some(name.into());
92 self
93 }
94
95 #[must_use]
97 pub fn version(mut self, version: impl Into<String>) -> Self {
98 self.version = Some(version.into());
99 self
100 }
101
102 #[must_use]
104 pub fn author(mut self, author: impl Into<String>) -> Self {
105 self.author = Some(author.into());
106 self
107 }
108
109 #[must_use]
111 pub fn license(mut self, license: impl Into<String>) -> Self {
112 self.license = Some(license.into());
113 self
114 }
115
116 #[must_use]
118 pub fn description(mut self, description: impl Into<String>) -> Self {
119 self.description = Some(description.into());
120 self
121 }
122
123 #[must_use]
125 pub const fn difficulty_levels(mut self, levels: u8) -> Self {
126 self.difficulty_levels = Some(levels);
127 self
128 }
129
130 #[must_use]
132 pub fn input_schema(mut self, schema: Schema) -> Self {
133 self.input_schema = Some(schema);
134 self
135 }
136
137 #[must_use]
139 pub fn output_schema(mut self, schema: Schema) -> Self {
140 self.output_schema = Some(schema);
141 self
142 }
143
144 pub fn build(self) -> Result<AprMetadata, AprError> {
150 let name = self.name.ok_or(AprError::MissingField { field: "name" })?;
151 let version_str = self
152 .version
153 .ok_or(AprError::MissingField { field: "version" })?;
154 let author = self
155 .author
156 .ok_or(AprError::MissingField { field: "author" })?;
157 let license = self
158 .license
159 .ok_or(AprError::MissingField { field: "license" })?;
160
161 if name.len() < 3 || name.len() > 50 {
163 return Err(AprError::InvalidName { name });
164 }
165 if !name
166 .chars()
167 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
168 {
169 return Err(AprError::InvalidName { name });
170 }
171
172 let version =
174 semver::Version::parse(&version_str).map_err(|_| AprError::InvalidVersion {
175 version: version_str,
176 })?;
177
178 Ok(AprMetadata {
179 name,
180 version,
181 author,
182 license,
183 description: self.description.unwrap_or_default(),
184 difficulty_levels: self.difficulty_levels,
185 input_schema: self.input_schema,
186 output_schema: self.output_schema,
187 file_size: 0,
188 created_at: Some(chrono::Utc::now()),
189 })
190 }
191}
192
193impl AprMetadata {
194 #[must_use]
196 pub fn builder() -> AprMetadataBuilder {
197 AprMetadataBuilder::new()
198 }
199
200 pub fn to_cbor(&self) -> Result<Vec<u8>, AprError> {
206 let mut buffer = Vec::new();
207 ciborium::into_writer(self, &mut buffer)
208 .map_err(|e| AprError::CborEncode(e.to_string()))?;
209 Ok(buffer)
210 }
211
212 pub fn from_cbor(bytes: &[u8]) -> Result<Self, AprError> {
218 ciborium::from_reader(bytes).map_err(|e| AprError::CborDecode(e.to_string()))
219 }
220}
221
222#[cfg(test)]
223#[allow(clippy::unwrap_used, clippy::expect_used)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_builder_all_required() {
229 let result = AprMetadata::builder()
230 .name("test")
231 .version("1.0.0")
232 .author("Author")
233 .license("MIT")
234 .build();
235
236 assert!(result.is_ok());
237 }
238
239 #[test]
240 fn test_builder_missing_name() {
241 let result = AprMetadata::builder()
242 .version("1.0.0")
243 .author("Author")
244 .license("MIT")
245 .build();
246
247 assert!(matches!(
248 result,
249 Err(AprError::MissingField { field: "name" })
250 ));
251 }
252
253 #[test]
254 fn test_name_too_short() {
255 let result = AprMetadata::builder()
256 .name("ab")
257 .version("1.0.0")
258 .author("Author")
259 .license("MIT")
260 .build();
261
262 assert!(matches!(result, Err(AprError::InvalidName { .. })));
263 }
264
265 #[test]
266 fn test_name_too_long() {
267 let long_name = "a".repeat(51);
268 let result = AprMetadata::builder()
269 .name(long_name)
270 .version("1.0.0")
271 .author("Author")
272 .license("MIT")
273 .build();
274
275 assert!(matches!(result, Err(AprError::InvalidName { .. })));
276 }
277
278 #[test]
279 fn test_name_invalid_chars() {
280 let result = AprMetadata::builder()
281 .name("test model!") .version("1.0.0")
283 .author("Author")
284 .license("MIT")
285 .build();
286
287 assert!(matches!(result, Err(AprError::InvalidName { .. })));
288 }
289
290 #[test]
291 fn test_invalid_version() {
292 let result = AprMetadata::builder()
293 .name("test")
294 .version("not.a.version")
295 .author("Author")
296 .license("MIT")
297 .build();
298
299 assert!(matches!(result, Err(AprError::InvalidVersion { .. })));
300 }
301
302 #[test]
303 fn test_cbor_roundtrip() {
304 let original = AprMetadata::builder()
305 .name("test-model")
306 .version("1.2.3")
307 .author("Test Author")
308 .license("MIT")
309 .description("A test description")
310 .difficulty_levels(5)
311 .build()
312 .expect("Should build");
313
314 let encoded = original.to_cbor().expect("Should encode");
315 let decoded = AprMetadata::from_cbor(&encoded).expect("Should decode");
316
317 assert_eq!(original.name, decoded.name);
318 assert_eq!(original.version, decoded.version);
319 assert_eq!(original.author, decoded.author);
320 assert_eq!(original.license, decoded.license);
321 assert_eq!(original.description, decoded.description);
322 assert_eq!(original.difficulty_levels, decoded.difficulty_levels);
323 }
324}