1use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::fmt;
14
15#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
20pub struct Metadata {
21 pub provenance: Option<Provenance>,
23 pub documentation: Option<Documentation>,
25 pub tags: HashSet<String>,
27 pub attributes: HashMap<String, String>,
29 pub version_history: Vec<VersionEntry>,
31}
32
33#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35pub struct Provenance {
36 pub created_by: String,
38 pub created_at: String,
40 pub source_file: Option<String>,
42 pub source_line: Option<usize>,
44 pub modified_by: Option<String>,
46 pub modified_at: Option<String>,
48 pub derived_from: Vec<String>,
50 pub notes: Option<String>,
52}
53
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct Documentation {
57 pub summary: String,
59 pub description: Option<String>,
61 pub examples: Vec<Example>,
63 pub notes: Vec<String>,
65 pub see_also: Vec<String>,
67}
68
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct Example {
72 pub title: String,
74 pub code: String,
76 pub expected_output: Option<String>,
78}
79
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
82pub struct VersionEntry {
83 pub version: String,
85 pub timestamp: String,
87 pub author: String,
89 pub changes: String,
91}
92
93impl Metadata {
94 pub fn new() -> Self {
96 Metadata::default()
97 }
98
99 pub fn with_provenance(provenance: Provenance) -> Self {
101 Metadata {
102 provenance: Some(provenance),
103 ..Default::default()
104 }
105 }
106
107 pub fn add_tag(&mut self, tag: impl Into<String>) {
109 self.tags.insert(tag.into());
110 }
111
112 pub fn remove_tag(&self, tag: &str) -> bool {
114 self.tags.contains(tag)
115 }
116
117 pub fn has_tag(&self, tag: &str) -> bool {
119 self.tags.contains(tag)
120 }
121
122 pub fn has_all_tags(&self, tags: &[String]) -> bool {
124 tags.iter().all(|tag| self.tags.contains(tag))
125 }
126
127 pub fn has_any_tag(&self, tags: &[String]) -> bool {
129 tags.iter().any(|tag| self.tags.contains(tag))
130 }
131
132 pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
134 self.attributes.insert(key.into(), value.into());
135 }
136
137 pub fn get_attribute(&self, key: &str) -> Option<&str> {
139 self.attributes.get(key).map(|s| s.as_str())
140 }
141
142 pub fn remove_attribute(&mut self, key: &str) -> Option<String> {
144 self.attributes.remove(key)
145 }
146
147 pub fn add_version(
149 &mut self,
150 version: impl Into<String>,
151 timestamp: impl Into<String>,
152 author: impl Into<String>,
153 changes: impl Into<String>,
154 ) {
155 self.version_history.push(VersionEntry {
156 version: version.into(),
157 timestamp: timestamp.into(),
158 author: author.into(),
159 changes: changes.into(),
160 });
161 }
162
163 pub fn latest_version(&self) -> Option<&VersionEntry> {
165 self.version_history.last()
166 }
167
168 pub fn set_documentation(&mut self, doc: Documentation) {
170 self.documentation = Some(doc);
171 }
172
173 pub fn get_summary(&self) -> Option<&str> {
175 self.documentation.as_ref().map(|d| d.summary.as_str())
176 }
177}
178
179impl Provenance {
180 pub fn new(created_by: impl Into<String>, created_at: impl Into<String>) -> Self {
182 Provenance {
183 created_by: created_by.into(),
184 created_at: created_at.into(),
185 source_file: None,
186 source_line: None,
187 modified_by: None,
188 modified_at: None,
189 derived_from: Vec::new(),
190 notes: None,
191 }
192 }
193
194 pub fn with_source(mut self, file: impl Into<String>, line: Option<usize>) -> Self {
196 self.source_file = Some(file.into());
197 self.source_line = line;
198 self
199 }
200
201 pub fn mark_modified(
203 &mut self,
204 modified_by: impl Into<String>,
205 modified_at: impl Into<String>,
206 ) {
207 self.modified_by = Some(modified_by.into());
208 self.modified_at = Some(modified_at.into());
209 }
210
211 pub fn add_derivation(&mut self, source: impl Into<String>) {
213 self.derived_from.push(source.into());
214 }
215
216 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
218 self.notes = Some(notes.into());
219 self
220 }
221}
222
223impl Documentation {
224 pub fn new(summary: impl Into<String>) -> Self {
226 Documentation {
227 summary: summary.into(),
228 description: None,
229 examples: Vec::new(),
230 notes: Vec::new(),
231 see_also: Vec::new(),
232 }
233 }
234
235 pub fn with_description(mut self, description: impl Into<String>) -> Self {
237 self.description = Some(description.into());
238 self
239 }
240
241 pub fn add_example(&mut self, example: Example) {
243 self.examples.push(example);
244 }
245
246 pub fn add_note(&mut self, note: impl Into<String>) {
248 self.notes.push(note.into());
249 }
250
251 pub fn add_see_also(&mut self, symbol: impl Into<String>) {
253 self.see_also.push(symbol.into());
254 }
255}
256
257impl Example {
258 pub fn new(title: impl Into<String>, code: impl Into<String>) -> Self {
260 Example {
261 title: title.into(),
262 code: code.into(),
263 expected_output: None,
264 }
265 }
266
267 pub fn with_output(mut self, output: impl Into<String>) -> Self {
269 self.expected_output = Some(output.into());
270 self
271 }
272}
273
274impl fmt::Display for Provenance {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 writeln!(f, "Created by: {} at {}", self.created_by, self.created_at)?;
277 if let Some(ref file) = self.source_file {
278 write!(f, "Source: {}", file)?;
279 if let Some(line) = self.source_line {
280 write!(f, ":{}", line)?;
281 }
282 writeln!(f)?;
283 }
284 if let Some(ref modified_by) = self.modified_by {
285 if let Some(ref modified_at) = self.modified_at {
286 writeln!(f, "Modified by: {} at {}", modified_by, modified_at)?;
287 }
288 }
289 if !self.derived_from.is_empty() {
290 writeln!(f, "Derived from: {}", self.derived_from.join(", "))?;
291 }
292 Ok(())
293 }
294}
295
296#[derive(Clone, Debug, Serialize, Deserialize)]
298pub struct TagCategory {
299 pub name: String,
301 pub description: Option<String>,
303 pub tags: HashSet<String>,
305}
306
307impl TagCategory {
308 pub fn new(name: impl Into<String>) -> Self {
310 TagCategory {
311 name: name.into(),
312 description: None,
313 tags: HashSet::new(),
314 }
315 }
316
317 pub fn with_description(mut self, description: impl Into<String>) -> Self {
319 self.description = Some(description.into());
320 self
321 }
322
323 pub fn add_tag(&mut self, tag: impl Into<String>) {
325 self.tags.insert(tag.into());
326 }
327
328 pub fn contains(&self, tag: &str) -> bool {
330 self.tags.contains(tag)
331 }
332}
333
334#[derive(Clone, Debug, Default, Serialize, Deserialize)]
336pub struct TagRegistry {
337 categories: HashMap<String, TagCategory>,
338}
339
340impl TagRegistry {
341 pub fn new() -> Self {
343 TagRegistry::default()
344 }
345
346 pub fn register_category(&mut self, category: TagCategory) {
348 self.categories.insert(category.name.clone(), category);
349 }
350
351 pub fn get_category(&self, name: &str) -> Option<&TagCategory> {
353 self.categories.get(name)
354 }
355
356 pub fn find_category_for_tag(&self, tag: &str) -> Option<&str> {
358 self.categories
359 .iter()
360 .find(|(_, cat)| cat.contains(tag))
361 .map(|(name, _)| name.as_str())
362 }
363
364 pub fn standard() -> Self {
366 let mut registry = TagRegistry::new();
367
368 let mut domain_cat =
369 TagCategory::new("domain").with_description("Tags related to problem domains");
370 domain_cat.add_tag("person");
371 domain_cat.add_tag("location");
372 domain_cat.add_tag("time");
373 domain_cat.add_tag("organization");
374 registry.register_category(domain_cat);
375
376 let mut status_cat =
377 TagCategory::new("status").with_description("Tags related to development status");
378 status_cat.add_tag("experimental");
379 status_cat.add_tag("stable");
380 status_cat.add_tag("deprecated");
381 registry.register_category(status_cat);
382
383 let mut application_cat =
384 TagCategory::new("application").with_description("Tags related to application areas");
385 application_cat.add_tag("reasoning");
386 application_cat.add_tag("learning");
387 application_cat.add_tag("planning");
388 application_cat.add_tag("inference");
389 registry.register_category(application_cat);
390
391 registry
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_metadata_tags() {
401 let mut meta = Metadata::new();
402 meta.add_tag("experimental");
403 meta.add_tag("reasoning");
404
405 assert!(meta.has_tag("experimental"));
406 assert!(meta.has_tag("reasoning"));
407 assert!(!meta.has_tag("stable"));
408
409 assert!(meta.has_all_tags(&["experimental".to_string(), "reasoning".to_string()]));
410 assert!(!meta.has_all_tags(&["experimental".to_string(), "stable".to_string()]));
411
412 assert!(meta.has_any_tag(&["experimental".to_string(), "stable".to_string()]));
413 }
414
415 #[test]
416 fn test_metadata_attributes() {
417 let mut meta = Metadata::new();
418 meta.set_attribute("complexity", "O(n^2)");
419 meta.set_attribute("author", "Alice");
420
421 assert_eq!(meta.get_attribute("complexity"), Some("O(n^2)"));
422 assert_eq!(meta.get_attribute("author"), Some("Alice"));
423 assert_eq!(meta.get_attribute("unknown"), None);
424
425 meta.remove_attribute("complexity");
426 assert_eq!(meta.get_attribute("complexity"), None);
427 }
428
429 #[test]
430 fn test_provenance() {
431 let prov = Provenance::new("Alice", "2025-01-15T10:30:00Z")
432 .with_source("rules.tl", Some(42))
433 .with_notes("Imported from legacy system");
434
435 assert_eq!(prov.created_by, "Alice");
436 assert_eq!(prov.created_at, "2025-01-15T10:30:00Z");
437 assert_eq!(prov.source_file, Some("rules.tl".to_string()));
438 assert_eq!(prov.source_line, Some(42));
439 }
440
441 #[test]
442 fn test_provenance_modification() {
443 let mut prov = Provenance::new("Alice", "2025-01-15T10:30:00Z");
444 prov.mark_modified("Bob", "2025-01-16T14:20:00Z");
445
446 assert_eq!(prov.modified_by, Some("Bob".to_string()));
447 assert_eq!(prov.modified_at, Some("2025-01-16T14:20:00Z".to_string()));
448 }
449
450 #[test]
451 fn test_provenance_derivation() {
452 let mut prov = Provenance::new("System", "2025-01-15T10:30:00Z");
453 prov.add_derivation("BaseRule");
454 prov.add_derivation("Optimization");
455
456 assert_eq!(prov.derived_from.len(), 2);
457 assert!(prov.derived_from.contains(&"BaseRule".to_string()));
458 }
459
460 #[test]
461 fn test_documentation() {
462 let mut doc = Documentation::new("A predicate for checking person relationships")
463 .with_description(
464 "This predicate checks if two persons have a specific relationship type",
465 );
466
467 doc.add_example(Example::new("Basic usage", "knows(alice, bob)"));
468 doc.add_note("This predicate is symmetric");
469 doc.add_see_also("friend");
470 doc.add_see_also("family");
471
472 assert_eq!(doc.summary, "A predicate for checking person relationships");
473 assert_eq!(doc.examples.len(), 1);
474 assert_eq!(doc.notes.len(), 1);
475 assert_eq!(doc.see_also.len(), 2);
476 }
477
478 #[test]
479 fn test_example() {
480 let example =
481 Example::new("Simple query", "Person(x)").with_output("[alice, bob, charlie]");
482
483 assert_eq!(example.title, "Simple query");
484 assert_eq!(example.code, "Person(x)");
485 assert_eq!(
486 example.expected_output,
487 Some("[alice, bob, charlie]".to_string())
488 );
489 }
490
491 #[test]
492 fn test_version_history() {
493 let mut meta = Metadata::new();
494 meta.add_version("1.0.0", "2025-01-15T10:00:00Z", "Alice", "Initial version");
495 meta.add_version("1.1.0", "2025-01-20T15:30:00Z", "Bob", "Added constraints");
496
497 assert_eq!(meta.version_history.len(), 2);
498
499 let latest = meta.latest_version().unwrap();
500 assert_eq!(latest.version, "1.1.0");
501 assert_eq!(latest.author, "Bob");
502 }
503
504 #[test]
505 fn test_tag_category() {
506 let mut category = TagCategory::new("domain").with_description("Problem domain tags");
507
508 category.add_tag("person");
509 category.add_tag("location");
510
511 assert_eq!(category.name, "domain");
512 assert!(category.contains("person"));
513 assert!(!category.contains("experimental"));
514 }
515
516 #[test]
517 fn test_tag_registry() {
518 let mut registry = TagRegistry::new();
519
520 let mut domain_cat = TagCategory::new("domain");
521 domain_cat.add_tag("person");
522 domain_cat.add_tag("location");
523 registry.register_category(domain_cat);
524
525 let category = registry.get_category("domain").unwrap();
526 assert!(category.contains("person"));
527
528 let found_category = registry.find_category_for_tag("person");
529 assert_eq!(found_category, Some("domain"));
530 }
531
532 #[test]
533 fn test_standard_tag_registry() {
534 let registry = TagRegistry::standard();
535
536 assert!(registry.get_category("domain").is_some());
537 assert!(registry.get_category("status").is_some());
538 assert!(registry.get_category("application").is_some());
539
540 assert_eq!(
541 registry.find_category_for_tag("experimental"),
542 Some("status")
543 );
544 assert_eq!(registry.find_category_for_tag("person"), Some("domain"));
545 assert_eq!(
546 registry.find_category_for_tag("reasoning"),
547 Some("application")
548 );
549 }
550}