Skip to main content

oxirs_gql/
api_versioning.rs

1//! Automated API Documentation Versioning
2//!
3//! This module provides automatic versioning for API documentation,
4//! tracking schema changes and maintaining versioned documentation.
5//!
6//! ## Features
7//!
8//! - **Version Management**: Semantic versioning for API
9//! - **Documentation Snapshots**: Store documentation at each version
10//! - **Version Comparison**: Compare documentation across versions
11//! - **Deprecation Tracking**: Track deprecated features across versions
12//! - **Migration Guides**: Generate guides for version upgrades
13
14use 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/// Semantic version
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct SemanticVersion {
24    /// Major version
25    pub major: u32,
26    /// Minor version
27    pub minor: u32,
28    /// Patch version
29    pub patch: u32,
30    /// Pre-release label
31    pub prerelease: Option<String>,
32}
33
34impl SemanticVersion {
35    /// Create a new version
36    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    /// Parse from string
46    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    /// Bump major version
70    pub fn bump_major(&self) -> Self {
71        Self::new(self.major + 1, 0, 0)
72    }
73
74    /// Bump minor version
75    pub fn bump_minor(&self) -> Self {
76        Self::new(self.major, self.minor + 1, 0)
77    }
78
79    /// Bump patch version
80    pub fn bump_patch(&self) -> Self {
81        Self::new(self.major, self.minor, self.patch + 1)
82    }
83
84    /// Compare versions
85    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    /// Check if this version is compatible with another
96    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/// Documentation entry
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct DocumentationEntry {
126    /// Entry path (e.g., "Query.users")
127    pub path: String,
128    /// Entry type
129    pub entry_type: EntryType,
130    /// Description
131    pub description: String,
132    /// Example
133    pub example: Option<String>,
134    /// Deprecated
135    pub deprecated: bool,
136    /// Deprecation message
137    pub deprecation_message: Option<String>,
138    /// Since version
139    pub since_version: Option<String>,
140    /// Until version (if removed)
141    pub until_version: Option<String>,
142    /// Tags
143    pub tags: Vec<String>,
144    /// Related entries
145    pub related: Vec<String>,
146}
147
148/// Entry type
149#[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/// Documentation snapshot
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct DocumentationSnapshot {
163    /// Version
164    pub version: SemanticVersion,
165    /// Timestamp
166    pub timestamp: SystemTime,
167    /// Entries
168    pub entries: HashMap<String, DocumentationEntry>,
169    /// Metadata
170    pub metadata: DocumentationMetadata,
171}
172
173impl DocumentationSnapshot {
174    /// Create a new snapshot
175    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    /// Add an entry
185    pub fn add_entry(&mut self, entry: DocumentationEntry) {
186        self.entries.insert(entry.path.clone(), entry);
187    }
188
189    /// Get entry by path
190    pub fn get_entry(&self, path: &str) -> Option<&DocumentationEntry> {
191        self.entries.get(path)
192    }
193
194    /// Get deprecated entries
195    pub fn get_deprecated_entries(&self) -> Vec<&DocumentationEntry> {
196        self.entries.values().filter(|e| e.deprecated).collect()
197    }
198}
199
200/// Documentation metadata
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct DocumentationMetadata {
203    /// Title
204    pub title: String,
205    /// Description
206    pub description: String,
207    /// Contact email
208    pub contact_email: Option<String>,
209    /// Terms of service URL
210    pub terms_of_service: Option<String>,
211    /// License
212    pub license: Option<String>,
213    /// Server URL
214    pub server_url: Option<String>,
215    /// Custom tags
216    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/// Documentation tag
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct DocumentationTag {
236    /// Tag name
237    pub name: String,
238    /// Description
239    pub description: String,
240}
241
242/// Version diff
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct VersionDiff {
245    /// From version
246    pub from_version: SemanticVersion,
247    /// To version
248    pub to_version: SemanticVersion,
249    /// Added entries
250    pub added: Vec<String>,
251    /// Removed entries
252    pub removed: Vec<String>,
253    /// Modified entries
254    pub modified: Vec<ModifiedEntry>,
255    /// Newly deprecated
256    pub newly_deprecated: Vec<String>,
257}
258
259/// Modified entry
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ModifiedEntry {
262    /// Path
263    pub path: String,
264    /// Changes
265    pub changes: Vec<String>,
266}
267
268/// Migration guide
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct MigrationGuide {
271    /// From version
272    pub from_version: SemanticVersion,
273    /// To version
274    pub to_version: SemanticVersion,
275    /// Steps
276    pub steps: Vec<MigrationStep>,
277    /// Breaking changes
278    pub breaking_changes: Vec<BreakingChange>,
279}
280
281/// Migration step
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct MigrationStep {
284    /// Step number
285    pub step: u32,
286    /// Title
287    pub title: String,
288    /// Description
289    pub description: String,
290    /// Code example
291    pub code_example: Option<CodeExample>,
292}
293
294/// Code example
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct CodeExample {
297    /// Before code
298    pub before: String,
299    /// After code
300    pub after: String,
301    /// Language
302    pub language: String,
303}
304
305/// Breaking change
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct BreakingChange {
308    /// Path affected
309    pub path: String,
310    /// Change type
311    pub change_type: String,
312    /// Description
313    pub description: String,
314    /// Workaround
315    pub workaround: Option<String>,
316}
317
318/// Internal state
319struct VersioningState {
320    /// All snapshots by version
321    snapshots: HashMap<SemanticVersion, DocumentationSnapshot>,
322    /// Version history (ordered)
323    version_history: Vec<SemanticVersion>,
324    /// Current version
325    current_version: Option<SemanticVersion>,
326    /// Migration guides
327    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
341/// API Documentation Versioning Manager
342///
343/// Manages versioned API documentation with snapshot storage
344/// and migration guide generation.
345pub struct ApiVersioningManager {
346    /// Internal state
347    state: Arc<RwLock<VersioningState>>,
348}
349
350impl ApiVersioningManager {
351    /// Create a new versioning manager
352    pub fn new() -> Self {
353        Self {
354            state: Arc::new(RwLock::new(VersioningState::new())),
355        }
356    }
357
358    /// Add a documentation snapshot
359    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    /// Get current version
377    pub async fn get_current_version(&self) -> Option<SemanticVersion> {
378        let state = self.state.read().await;
379        state.current_version.clone()
380    }
381
382    /// Get all versions
383    pub async fn get_all_versions(&self) -> Vec<SemanticVersion> {
384        let state = self.state.read().await;
385        state.version_history.clone()
386    }
387
388    /// Get snapshot by version
389    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    /// Get latest snapshot
395    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    /// Compare two versions
405    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    /// Generate migration guide
476    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        // Steps for removed entries
487        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        // Steps for deprecated entries
501        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        // Breaking changes
515        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    /// Store migration guide
535    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    /// Get migration guide
542    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    /// Generate documentation index
555    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    /// Export version as OpenAPI-compatible JSON
578    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    /// Get entries by tag
612    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    /// Get next suggested version
629    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}