1use anyhow::{anyhow, Result};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::time::SystemTime;
19use tokio::sync::RwLock;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct SemanticVersion {
24 pub major: u32,
26 pub minor: u32,
28 pub patch: u32,
30 pub prerelease: Option<String>,
32}
33
34impl SemanticVersion {
35 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
37 Self {
38 major,
39 minor,
40 patch,
41 prerelease: None,
42 }
43 }
44
45 pub fn parse(s: &str) -> Result<Self> {
47 let parts: Vec<&str> = s.split('-').collect();
48 let version_parts = parts[0];
49 let prerelease = parts.get(1).map(|s| s.to_string());
50
51 let nums: Vec<u32> = version_parts
52 .split('.')
53 .map(|p| p.parse())
54 .collect::<Result<Vec<_>, _>>()
55 .map_err(|_| anyhow!("Invalid version format: {}", s))?;
56
57 if nums.len() != 3 {
58 return Err(anyhow!("Version must have 3 parts: {}", s));
59 }
60
61 Ok(Self {
62 major: nums[0],
63 minor: nums[1],
64 patch: nums[2],
65 prerelease,
66 })
67 }
68
69 pub fn bump_major(&self) -> Self {
71 Self::new(self.major + 1, 0, 0)
72 }
73
74 pub fn bump_minor(&self) -> Self {
76 Self::new(self.major, self.minor + 1, 0)
77 }
78
79 pub fn bump_patch(&self) -> Self {
81 Self::new(self.major, self.minor, self.patch + 1)
82 }
83
84 pub fn compare(&self, other: &Self) -> std::cmp::Ordering {
86 match self.major.cmp(&other.major) {
87 std::cmp::Ordering::Equal => match self.minor.cmp(&other.minor) {
88 std::cmp::Ordering::Equal => self.patch.cmp(&other.patch),
89 ord => ord,
90 },
91 ord => ord,
92 }
93 }
94
95 pub fn is_compatible_with(&self, other: &Self) -> bool {
97 self.major == other.major
98 }
99}
100
101impl std::fmt::Display for SemanticVersion {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 if let Some(pre) = &self.prerelease {
104 write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre)
105 } else {
106 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
107 }
108 }
109}
110
111impl PartialOrd for SemanticVersion {
112 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
113 Some(std::cmp::Ord::cmp(self, other))
114 }
115}
116
117impl Ord for SemanticVersion {
118 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
119 self.compare(other)
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct DocumentationEntry {
126 pub path: String,
128 pub entry_type: EntryType,
130 pub description: String,
132 pub example: Option<String>,
134 pub deprecated: bool,
136 pub deprecation_message: Option<String>,
138 pub since_version: Option<String>,
140 pub until_version: Option<String>,
142 pub tags: Vec<String>,
144 pub related: Vec<String>,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub enum EntryType {
151 Type,
152 Field,
153 Argument,
154 Enum,
155 EnumValue,
156 Directive,
157 Scalar,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct DocumentationSnapshot {
163 pub version: SemanticVersion,
165 pub timestamp: SystemTime,
167 pub entries: HashMap<String, DocumentationEntry>,
169 pub metadata: DocumentationMetadata,
171}
172
173impl DocumentationSnapshot {
174 pub fn new(version: SemanticVersion) -> Self {
176 Self {
177 version,
178 timestamp: SystemTime::now(),
179 entries: HashMap::new(),
180 metadata: DocumentationMetadata::default(),
181 }
182 }
183
184 pub fn add_entry(&mut self, entry: DocumentationEntry) {
186 self.entries.insert(entry.path.clone(), entry);
187 }
188
189 pub fn get_entry(&self, path: &str) -> Option<&DocumentationEntry> {
191 self.entries.get(path)
192 }
193
194 pub fn get_deprecated_entries(&self) -> Vec<&DocumentationEntry> {
196 self.entries.values().filter(|e| e.deprecated).collect()
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct DocumentationMetadata {
203 pub title: String,
205 pub description: String,
207 pub contact_email: Option<String>,
209 pub terms_of_service: Option<String>,
211 pub license: Option<String>,
213 pub server_url: Option<String>,
215 pub tags: Vec<DocumentationTag>,
217}
218
219impl Default for DocumentationMetadata {
220 fn default() -> Self {
221 Self {
222 title: "GraphQL API".to_string(),
223 description: "GraphQL API documentation".to_string(),
224 contact_email: None,
225 terms_of_service: None,
226 license: None,
227 server_url: None,
228 tags: Vec::new(),
229 }
230 }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct DocumentationTag {
236 pub name: String,
238 pub description: String,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct VersionDiff {
245 pub from_version: SemanticVersion,
247 pub to_version: SemanticVersion,
249 pub added: Vec<String>,
251 pub removed: Vec<String>,
253 pub modified: Vec<ModifiedEntry>,
255 pub newly_deprecated: Vec<String>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ModifiedEntry {
262 pub path: String,
264 pub changes: Vec<String>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct MigrationGuide {
271 pub from_version: SemanticVersion,
273 pub to_version: SemanticVersion,
275 pub steps: Vec<MigrationStep>,
277 pub breaking_changes: Vec<BreakingChange>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct MigrationStep {
284 pub step: u32,
286 pub title: String,
288 pub description: String,
290 pub code_example: Option<CodeExample>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct CodeExample {
297 pub before: String,
299 pub after: String,
301 pub language: String,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct BreakingChange {
308 pub path: String,
310 pub change_type: String,
312 pub description: String,
314 pub workaround: Option<String>,
316}
317
318struct VersioningState {
320 snapshots: HashMap<SemanticVersion, DocumentationSnapshot>,
322 version_history: Vec<SemanticVersion>,
324 current_version: Option<SemanticVersion>,
326 migration_guides: HashMap<(SemanticVersion, SemanticVersion), MigrationGuide>,
328}
329
330impl VersioningState {
331 fn new() -> Self {
332 Self {
333 snapshots: HashMap::new(),
334 version_history: Vec::new(),
335 current_version: None,
336 migration_guides: HashMap::new(),
337 }
338 }
339}
340
341pub struct ApiVersioningManager {
346 state: Arc<RwLock<VersioningState>>,
348}
349
350impl ApiVersioningManager {
351 pub fn new() -> Self {
353 Self {
354 state: Arc::new(RwLock::new(VersioningState::new())),
355 }
356 }
357
358 pub async fn add_snapshot(&self, snapshot: DocumentationSnapshot) -> Result<()> {
360 let mut state = self.state.write().await;
361
362 let version = snapshot.version.clone();
363
364 if state.snapshots.contains_key(&version) {
365 return Err(anyhow!("Version {} already exists", version));
366 }
367
368 state.snapshots.insert(version.clone(), snapshot);
369 state.version_history.push(version.clone());
370 state.version_history.sort();
371 state.current_version = Some(version);
372
373 Ok(())
374 }
375
376 pub async fn get_current_version(&self) -> Option<SemanticVersion> {
378 let state = self.state.read().await;
379 state.current_version.clone()
380 }
381
382 pub async fn get_all_versions(&self) -> Vec<SemanticVersion> {
384 let state = self.state.read().await;
385 state.version_history.clone()
386 }
387
388 pub async fn get_snapshot(&self, version: &SemanticVersion) -> Option<DocumentationSnapshot> {
390 let state = self.state.read().await;
391 state.snapshots.get(version).cloned()
392 }
393
394 pub async fn get_latest_snapshot(&self) -> Option<DocumentationSnapshot> {
396 let state = self.state.read().await;
397 state
398 .current_version
399 .as_ref()
400 .and_then(|v| state.snapshots.get(v))
401 .cloned()
402 }
403
404 pub async fn compare_versions(
406 &self,
407 from: &SemanticVersion,
408 to: &SemanticVersion,
409 ) -> Result<VersionDiff> {
410 let state = self.state.read().await;
411
412 let from_snapshot = state
413 .snapshots
414 .get(from)
415 .ok_or_else(|| anyhow!("Version {} not found", from))?;
416
417 let to_snapshot = state
418 .snapshots
419 .get(to)
420 .ok_or_else(|| anyhow!("Version {} not found", to))?;
421
422 let from_paths: std::collections::HashSet<_> = from_snapshot.entries.keys().collect();
423 let to_paths: std::collections::HashSet<_> = to_snapshot.entries.keys().collect();
424
425 let added: Vec<String> = to_paths
426 .difference(&from_paths)
427 .map(|s| (*s).clone())
428 .collect();
429 let removed: Vec<String> = from_paths
430 .difference(&to_paths)
431 .map(|s| (*s).clone())
432 .collect();
433
434 let mut modified = Vec::new();
435 let mut newly_deprecated = Vec::new();
436
437 for path in from_paths.intersection(&to_paths) {
438 let from_entry = &from_snapshot.entries[*path];
439 let to_entry = &to_snapshot.entries[*path];
440
441 let mut changes = Vec::new();
442
443 if from_entry.description != to_entry.description {
444 changes.push("Description changed".to_string());
445 }
446
447 if from_entry.deprecated != to_entry.deprecated {
448 if to_entry.deprecated {
449 newly_deprecated.push((*path).clone());
450 }
451 changes.push(format!(
452 "Deprecated: {} -> {}",
453 from_entry.deprecated, to_entry.deprecated
454 ));
455 }
456
457 if !changes.is_empty() {
458 modified.push(ModifiedEntry {
459 path: (*path).clone(),
460 changes,
461 });
462 }
463 }
464
465 Ok(VersionDiff {
466 from_version: from.clone(),
467 to_version: to.clone(),
468 added,
469 removed,
470 modified,
471 newly_deprecated,
472 })
473 }
474
475 pub async fn generate_migration_guide(
477 &self,
478 from: &SemanticVersion,
479 to: &SemanticVersion,
480 ) -> Result<MigrationGuide> {
481 let diff = self.compare_versions(from, to).await?;
482
483 let mut steps = Vec::new();
484 let mut step_num = 1;
485
486 for path in &diff.removed {
488 steps.push(MigrationStep {
489 step: step_num,
490 title: format!("Handle removed: {}", path),
491 description: format!(
492 "The entry '{}' has been removed. Update your code to no longer use this.",
493 path
494 ),
495 code_example: None,
496 });
497 step_num += 1;
498 }
499
500 for path in &diff.newly_deprecated {
502 steps.push(MigrationStep {
503 step: step_num,
504 title: format!("Update deprecated: {}", path),
505 description: format!(
506 "The entry '{}' is now deprecated. Consider migrating to the recommended alternative.",
507 path
508 ),
509 code_example: None,
510 });
511 step_num += 1;
512 }
513
514 let breaking_changes: Vec<BreakingChange> = diff
516 .removed
517 .iter()
518 .map(|path| BreakingChange {
519 path: path.clone(),
520 change_type: "Removed".to_string(),
521 description: format!("Entry '{}' was removed", path),
522 workaround: None,
523 })
524 .collect();
525
526 Ok(MigrationGuide {
527 from_version: from.clone(),
528 to_version: to.clone(),
529 steps,
530 breaking_changes,
531 })
532 }
533
534 pub async fn store_migration_guide(&self, guide: MigrationGuide) {
536 let mut state = self.state.write().await;
537 let key = (guide.from_version.clone(), guide.to_version.clone());
538 state.migration_guides.insert(key, guide);
539 }
540
541 pub async fn get_migration_guide(
543 &self,
544 from: &SemanticVersion,
545 to: &SemanticVersion,
546 ) -> Option<MigrationGuide> {
547 let state = self.state.read().await;
548 state
549 .migration_guides
550 .get(&(from.clone(), to.clone()))
551 .cloned()
552 }
553
554 pub async fn generate_index(&self) -> String {
556 let state = self.state.read().await;
557
558 let mut index = String::new();
559 index.push_str("# API Documentation Versions\n\n");
560
561 for version in state.version_history.iter().rev() {
562 if let Some(snapshot) = state.snapshots.get(version) {
563 index.push_str(&format!("## Version {}\n\n", version));
564 index.push_str(&format!("- **Title**: {}\n", snapshot.metadata.title));
565 index.push_str(&format!("- **Entries**: {}\n", snapshot.entries.len()));
566 index.push_str(&format!(
567 "- **Deprecated**: {}\n",
568 snapshot.get_deprecated_entries().len()
569 ));
570 index.push('\n');
571 }
572 }
573
574 index
575 }
576
577 pub async fn export_openapi(&self, version: &SemanticVersion) -> Result<String> {
579 let state = self.state.read().await;
580
581 let snapshot = state
582 .snapshots
583 .get(version)
584 .ok_or_else(|| anyhow!("Version {} not found", version))?;
585
586 let openapi = serde_json::json!({
587 "openapi": "3.0.0",
588 "info": {
589 "title": snapshot.metadata.title,
590 "description": snapshot.metadata.description,
591 "version": version.to_string(),
592 "contact": {
593 "email": snapshot.metadata.contact_email
594 },
595 "license": {
596 "name": snapshot.metadata.license
597 }
598 },
599 "servers": [{
600 "url": snapshot.metadata.server_url
601 }],
602 "paths": {},
603 "components": {
604 "schemas": {}
605 }
606 });
607
608 Ok(serde_json::to_string_pretty(&openapi)?)
609 }
610
611 pub async fn get_entries_by_tag(&self, version: &SemanticVersion, tag: &str) -> Vec<String> {
613 let state = self.state.read().await;
614
615 state
616 .snapshots
617 .get(version)
618 .map(|s| {
619 s.entries
620 .iter()
621 .filter(|(_, e)| e.tags.contains(&tag.to_string()))
622 .map(|(k, _)| k.clone())
623 .collect()
624 })
625 .unwrap_or_default()
626 }
627
628 pub async fn suggest_next_version(&self, has_breaking_changes: bool) -> SemanticVersion {
630 let state = self.state.read().await;
631
632 let current = state
633 .current_version
634 .as_ref()
635 .cloned()
636 .unwrap_or_else(|| SemanticVersion::new(0, 1, 0));
637
638 if has_breaking_changes {
639 current.bump_major()
640 } else {
641 current.bump_minor()
642 }
643 }
644}
645
646impl Default for ApiVersioningManager {
647 fn default() -> Self {
648 Self::new()
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn test_semantic_version_parse() {
658 let v = SemanticVersion::parse("1.2.3").expect("should succeed");
659 assert_eq!(v.major, 1);
660 assert_eq!(v.minor, 2);
661 assert_eq!(v.patch, 3);
662 assert_eq!(v.to_string(), "1.2.3");
663
664 let v = SemanticVersion::parse("1.0.0-beta").expect("should succeed");
665 assert_eq!(v.prerelease, Some("beta".to_string()));
666 }
667
668 #[test]
669 fn test_version_bumping() {
670 let v = SemanticVersion::new(1, 2, 3);
671
672 let major = v.bump_major();
673 assert_eq!(major.to_string(), "2.0.0");
674
675 let minor = v.bump_minor();
676 assert_eq!(minor.to_string(), "1.3.0");
677
678 let patch = v.bump_patch();
679 assert_eq!(patch.to_string(), "1.2.4");
680 }
681
682 #[test]
683 fn test_version_comparison() {
684 let v1 = SemanticVersion::new(1, 0, 0);
685 let v2 = SemanticVersion::new(2, 0, 0);
686 let v3 = SemanticVersion::new(1, 1, 0);
687
688 assert!(v1 < v2);
689 assert!(v1 < v3);
690 assert!(v3 < v2);
691 }
692
693 #[test]
694 fn test_version_compatibility() {
695 let v1 = SemanticVersion::new(1, 0, 0);
696 let v2 = SemanticVersion::new(1, 5, 0);
697 let v3 = SemanticVersion::new(2, 0, 0);
698
699 assert!(v1.is_compatible_with(&v2));
700 assert!(!v1.is_compatible_with(&v3));
701 }
702
703 #[tokio::test]
704 async fn test_add_snapshot() {
705 let manager = ApiVersioningManager::new();
706
707 let version = SemanticVersion::new(1, 0, 0);
708 let snapshot = DocumentationSnapshot::new(version.clone());
709
710 manager
711 .add_snapshot(snapshot)
712 .await
713 .expect("should succeed");
714
715 let current = manager.get_current_version().await;
716 assert_eq!(current, Some(version));
717 }
718
719 #[tokio::test]
720 async fn test_compare_versions() {
721 let manager = ApiVersioningManager::new();
722
723 let v1 = SemanticVersion::new(1, 0, 0);
724 let mut snap1 = DocumentationSnapshot::new(v1.clone());
725 snap1.add_entry(DocumentationEntry {
726 path: "Query.users".to_string(),
727 entry_type: EntryType::Field,
728 description: "Get users".to_string(),
729 example: None,
730 deprecated: false,
731 deprecation_message: None,
732 since_version: None,
733 until_version: None,
734 tags: Vec::new(),
735 related: Vec::new(),
736 });
737 manager.add_snapshot(snap1).await.expect("should succeed");
738
739 let v2 = SemanticVersion::new(1, 1, 0);
740 let mut snap2 = DocumentationSnapshot::new(v2.clone());
741 snap2.add_entry(DocumentationEntry {
742 path: "Query.users".to_string(),
743 entry_type: EntryType::Field,
744 description: "Get all users".to_string(),
745 example: None,
746 deprecated: false,
747 deprecation_message: None,
748 since_version: None,
749 until_version: None,
750 tags: Vec::new(),
751 related: Vec::new(),
752 });
753 snap2.add_entry(DocumentationEntry {
754 path: "Query.posts".to_string(),
755 entry_type: EntryType::Field,
756 description: "Get posts".to_string(),
757 example: None,
758 deprecated: false,
759 deprecation_message: None,
760 since_version: None,
761 until_version: None,
762 tags: Vec::new(),
763 related: Vec::new(),
764 });
765 manager.add_snapshot(snap2).await.expect("should succeed");
766
767 let diff = manager
768 .compare_versions(&v1, &v2)
769 .await
770 .expect("should succeed");
771
772 assert_eq!(diff.added.len(), 1);
773 assert!(diff.added.contains(&"Query.posts".to_string()));
774 assert_eq!(diff.modified.len(), 1);
775 }
776
777 #[tokio::test]
778 async fn test_migration_guide_generation() {
779 let manager = ApiVersioningManager::new();
780
781 let v1 = SemanticVersion::new(1, 0, 0);
782 let mut snap1 = DocumentationSnapshot::new(v1.clone());
783 snap1.add_entry(DocumentationEntry {
784 path: "Query.oldField".to_string(),
785 entry_type: EntryType::Field,
786 description: "Old field".to_string(),
787 example: None,
788 deprecated: false,
789 deprecation_message: None,
790 since_version: None,
791 until_version: None,
792 tags: Vec::new(),
793 related: Vec::new(),
794 });
795 manager.add_snapshot(snap1).await.expect("should succeed");
796
797 let v2 = SemanticVersion::new(2, 0, 0);
798 let snap2 = DocumentationSnapshot::new(v2.clone());
799 manager.add_snapshot(snap2).await.expect("should succeed");
800
801 let guide = manager
802 .generate_migration_guide(&v1, &v2)
803 .await
804 .expect("should succeed");
805
806 assert!(!guide.steps.is_empty());
807 assert!(!guide.breaking_changes.is_empty());
808 }
809
810 #[tokio::test]
811 async fn test_suggest_next_version() {
812 let manager = ApiVersioningManager::new();
813
814 let v1 = SemanticVersion::new(1, 0, 0);
815 let snap1 = DocumentationSnapshot::new(v1.clone());
816 manager.add_snapshot(snap1).await.expect("should succeed");
817
818 let next_minor = manager.suggest_next_version(false).await;
819 assert_eq!(next_minor.to_string(), "1.1.0");
820
821 let next_major = manager.suggest_next_version(true).await;
822 assert_eq!(next_major.to_string(), "2.0.0");
823 }
824
825 #[tokio::test]
826 async fn test_get_deprecated_entries() {
827 let v1 = SemanticVersion::new(1, 0, 0);
828 let mut snapshot = DocumentationSnapshot::new(v1);
829
830 snapshot.add_entry(DocumentationEntry {
831 path: "Query.oldField".to_string(),
832 entry_type: EntryType::Field,
833 description: "Old field".to_string(),
834 example: None,
835 deprecated: true,
836 deprecation_message: Some("Use newField instead".to_string()),
837 since_version: None,
838 until_version: None,
839 tags: Vec::new(),
840 related: Vec::new(),
841 });
842
843 snapshot.add_entry(DocumentationEntry {
844 path: "Query.newField".to_string(),
845 entry_type: EntryType::Field,
846 description: "New field".to_string(),
847 example: None,
848 deprecated: false,
849 deprecation_message: None,
850 since_version: None,
851 until_version: None,
852 tags: Vec::new(),
853 related: Vec::new(),
854 });
855
856 let deprecated = snapshot.get_deprecated_entries();
857 assert_eq!(deprecated.len(), 1);
858 assert_eq!(deprecated[0].path, "Query.oldField");
859 }
860
861 #[tokio::test]
862 async fn test_generate_index() {
863 let manager = ApiVersioningManager::new();
864
865 let v1 = SemanticVersion::new(1, 0, 0);
866 let snap1 = DocumentationSnapshot::new(v1);
867 manager.add_snapshot(snap1).await.expect("should succeed");
868
869 let index = manager.generate_index().await;
870 assert!(index.contains("Version 1.0.0"));
871 }
872}