Skip to main content

torsh_core/
api_compat.rs

1//! API Compatibility and Deprecation Management
2//!
3//! This module provides infrastructure for managing API evolution,
4//! deprecation warnings, and compatibility tracking across ToRSh versions.
5//!
6//! # Features
7//!
8//! - **Deprecation Warnings**: Track and emit warnings for deprecated APIs
9//! - **Version Compatibility**: Check compatibility between ToRSh versions
10//! - **Migration Guides**: Provide automated migration suggestions
11//! - **Breaking Change Detection**: Identify breaking changes in API usage
12//!
13//! # Examples
14//!
15//! ```rust
16//! use torsh_core::api_compat::{deprecation_warning, deprecation_warning_inline, Version};
17//!
18//! // Simple deprecation warning (requires prior registration)
19//! deprecation_warning("old_function");
20//!
21//! // Emit a deprecation warning with inline info
22//! deprecation_warning_inline(
23//!     "another_old_function",
24//!     Version::new(0, 1, 0),
25//!     Version::new(0, 2, 0),
26//!     Some("new_function")
27//! );
28//! ```
29
30use std::collections::HashMap;
31use std::fmt;
32use std::sync::{Arc, Mutex, OnceLock};
33
34/// Semantic version representation
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct Version {
37    pub major: u16,
38    pub minor: u16,
39    pub patch: u16,
40}
41
42impl Version {
43    /// Create a new version
44    pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
45        Self {
46            major,
47            minor,
48            patch,
49        }
50    }
51
52    /// Parse version from string (e.g., "0.1.0")
53    pub fn parse(s: &str) -> Option<Self> {
54        let parts: Vec<&str> = s.split('.').collect();
55        if parts.len() != 3 {
56            return None;
57        }
58
59        let major = parts[0].parse().ok()?;
60        let minor = parts[1].parse().ok()?;
61        let patch = parts[2].parse().ok()?;
62
63        Some(Self::new(major, minor, patch))
64    }
65
66    /// Check if this version is compatible with another version
67    /// using semantic versioning rules
68    pub fn is_compatible_with(&self, other: &Version) -> bool {
69        // Major version must match for compatibility
70        if self.major != other.major {
71            return false;
72        }
73
74        // If major is 0, minor version must also match
75        if self.major == 0 && self.minor != other.minor {
76            return false;
77        }
78
79        true
80    }
81}
82
83impl fmt::Display for Version {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
86    }
87}
88
89/// Current ToRSh version
90pub const TORSH_VERSION: Version = Version::new(0, 1, 0);
91
92/// Deprecation severity level
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub enum DeprecationSeverity {
95    /// Soft deprecation - will be removed in future major version
96    Soft,
97    /// Hard deprecation - will be removed in next minor version
98    Hard,
99    /// Critical - will be removed in next patch version
100    Critical,
101}
102
103/// Information about a deprecated API
104#[derive(Debug, Clone)]
105pub struct DeprecationInfo {
106    /// Name of the deprecated API
107    pub api_name: String,
108    /// Version when API was deprecated
109    pub deprecated_in: Version,
110    /// Version when API will be removed
111    pub removed_in: Version,
112    /// Suggested replacement
113    pub replacement: Option<String>,
114    /// Deprecation reason
115    pub reason: Option<String>,
116    /// Migration guide URL or text
117    pub migration_guide: Option<String>,
118    /// Severity of deprecation
119    pub severity: DeprecationSeverity,
120}
121
122impl DeprecationInfo {
123    /// Create a new deprecation info
124    pub fn new(api_name: impl Into<String>, deprecated_in: Version, removed_in: Version) -> Self {
125        Self {
126            api_name: api_name.into(),
127            deprecated_in,
128            removed_in,
129            replacement: None,
130            reason: None,
131            migration_guide: None,
132            severity: DeprecationSeverity::Soft,
133        }
134    }
135
136    /// Set the replacement API
137    pub fn with_replacement(mut self, replacement: impl Into<String>) -> Self {
138        self.replacement = Some(replacement.into());
139        self
140    }
141
142    /// Set the deprecation reason
143    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
144        self.reason = Some(reason.into());
145        self
146    }
147
148    /// Set the migration guide
149    pub fn with_migration_guide(mut self, guide: impl Into<String>) -> Self {
150        self.migration_guide = Some(guide.into());
151        self
152    }
153
154    /// Set the severity
155    pub fn with_severity(mut self, severity: DeprecationSeverity) -> Self {
156        self.severity = severity;
157        self
158    }
159
160    /// Check if this API has been removed in the current version
161    pub fn is_removed(&self) -> bool {
162        TORSH_VERSION >= self.removed_in
163    }
164
165    /// Check if this API should show a warning
166    pub fn should_warn(&self) -> bool {
167        TORSH_VERSION >= self.deprecated_in && TORSH_VERSION < self.removed_in
168    }
169
170    /// Format a deprecation warning message
171    pub fn format_warning(&self) -> String {
172        let mut msg = format!(
173            "API '{}' is deprecated since version {} and will be removed in version {}",
174            self.api_name, self.deprecated_in, self.removed_in
175        );
176
177        if let Some(ref replacement) = self.replacement {
178            msg.push_str(&format!(". Use '{}' instead", replacement));
179        }
180
181        if let Some(ref reason) = self.reason {
182            msg.push_str(&format!(". Reason: {}", reason));
183        }
184
185        if let Some(ref guide) = self.migration_guide {
186            msg.push_str(&format!(". Migration guide: {}", guide));
187        }
188
189        msg
190    }
191}
192
193/// Global deprecation tracker
194static DEPRECATION_TRACKER: OnceLock<Arc<Mutex<DeprecationTracker>>> = OnceLock::new();
195
196/// Tracks deprecation warnings and API usage
197struct DeprecationTracker {
198    /// Registered deprecations
199    deprecations: HashMap<String, DeprecationInfo>,
200    /// Count of warnings emitted per API
201    warning_counts: HashMap<String, usize>,
202    /// Maximum warnings per API before suppressing
203    max_warnings_per_api: usize,
204    /// Whether to emit warnings to stderr
205    emit_warnings: bool,
206}
207
208impl Default for DeprecationTracker {
209    fn default() -> Self {
210        Self {
211            deprecations: HashMap::new(),
212            warning_counts: HashMap::new(),
213            max_warnings_per_api: 10,   // Limit repeated warnings
214            emit_warnings: !cfg!(test), // Suppress in tests by default
215        }
216    }
217}
218
219impl DeprecationTracker {
220    /// Register a deprecated API
221    fn register(&mut self, info: DeprecationInfo) {
222        self.deprecations.insert(info.api_name.clone(), info);
223    }
224
225    /// Emit a deprecation warning
226    fn emit_warning(&mut self, api_name: &str) -> bool {
227        // Check if API is registered
228        let info = match self.deprecations.get(api_name) {
229            Some(info) => info,
230            None => return false,
231        };
232
233        // Check if we should warn
234        if !info.should_warn() {
235            return false;
236        }
237
238        // Check if we've exceeded max warnings
239        let count = self.warning_counts.entry(api_name.to_string()).or_insert(0);
240        if *count >= self.max_warnings_per_api {
241            return false;
242        }
243        *count += 1;
244
245        // Emit warning if enabled
246        if self.emit_warnings {
247            eprintln!("⚠️  DEPRECATION WARNING: {}", info.format_warning());
248
249            if *count == self.max_warnings_per_api {
250                eprintln!(
251                    "⚠️  (Further warnings for '{}' will be suppressed)",
252                    api_name
253                );
254            }
255        }
256
257        true
258    }
259
260    /// Get deprecation info for an API
261    fn get_info(&self, api_name: &str) -> Option<&DeprecationInfo> {
262        self.deprecations.get(api_name)
263    }
264
265    /// Get all registered deprecations
266    fn get_all_deprecations(&self) -> Vec<DeprecationInfo> {
267        self.deprecations.values().cloned().collect()
268    }
269
270    /// Get warning statistics
271    fn get_warning_stats(&self) -> HashMap<String, usize> {
272        self.warning_counts.clone()
273    }
274
275    /// Clear warning counts
276    fn clear_warning_counts(&mut self) {
277        self.warning_counts.clear();
278    }
279
280    /// Set whether to emit warnings
281    fn set_emit_warnings(&mut self, emit: bool) {
282        self.emit_warnings = emit;
283    }
284
285    /// Set maximum warnings per API
286    fn set_max_warnings_per_api(&mut self, max: usize) {
287        self.max_warnings_per_api = max;
288    }
289}
290
291/// Get the global deprecation tracker
292fn get_tracker() -> Arc<Mutex<DeprecationTracker>> {
293    DEPRECATION_TRACKER
294        .get_or_init(|| Arc::new(Mutex::new(DeprecationTracker::default())))
295        .clone()
296}
297
298/// Register a deprecated API
299///
300/// # Examples
301///
302/// ```rust
303/// use torsh_core::api_compat::{register_deprecation, DeprecationInfo, Version};
304///
305/// let info = DeprecationInfo::new("old_function", Version::new(0, 1, 0), Version::new(0, 2, 0))
306///     .with_replacement("new_function")
307///     .with_reason("Improved performance and API consistency");
308///
309/// register_deprecation(info);
310/// ```
311pub fn register_deprecation(info: DeprecationInfo) {
312    get_tracker()
313        .lock()
314        .expect("lock should not be poisoned")
315        .register(info);
316}
317
318/// Emit a deprecation warning for an API
319///
320/// # Examples
321///
322/// ```rust
323/// use torsh_core::api_compat::deprecation_warning;
324///
325/// deprecation_warning("old_function");
326/// ```
327pub fn deprecation_warning(api_name: &str) -> bool {
328    get_tracker()
329        .lock()
330        .expect("lock should not be poisoned")
331        .emit_warning(api_name)
332}
333
334/// Convenience function to emit a deprecation warning with inline info
335///
336/// # Examples
337///
338/// ```rust
339/// use torsh_core::api_compat::{deprecation_warning_inline, Version};
340///
341/// deprecation_warning_inline(
342///     "old_function",
343///     Version::new(0, 1, 0),
344///     Version::new(0, 2, 0),
345///     Some("new_function")
346/// );
347/// ```
348pub fn deprecation_warning_inline(
349    api_name: &str,
350    deprecated_in: Version,
351    removed_in: Version,
352    replacement: Option<&str>,
353) {
354    let mut info = DeprecationInfo::new(api_name, deprecated_in, removed_in);
355    if let Some(repl) = replacement {
356        info = info.with_replacement(repl);
357    }
358    register_deprecation(info);
359    deprecation_warning(api_name);
360}
361
362/// Get deprecation info for an API
363pub fn get_deprecation_info(api_name: &str) -> Option<DeprecationInfo> {
364    get_tracker()
365        .lock()
366        .expect("lock should not be poisoned")
367        .get_info(api_name)
368        .cloned()
369}
370
371/// Get all registered deprecations
372pub fn get_all_deprecations() -> Vec<DeprecationInfo> {
373    get_tracker()
374        .lock()
375        .expect("lock should not be poisoned")
376        .get_all_deprecations()
377}
378
379/// Get deprecation warning statistics
380pub fn get_deprecation_stats() -> HashMap<String, usize> {
381    get_tracker()
382        .lock()
383        .expect("lock should not be poisoned")
384        .get_warning_stats()
385}
386
387/// Clear deprecation warning counts
388pub fn clear_deprecation_counts() {
389    get_tracker()
390        .lock()
391        .expect("lock should not be poisoned")
392        .clear_warning_counts();
393}
394
395/// Configure deprecation warning behavior
396pub fn configure_deprecation_warnings(emit: bool, max_per_api: usize) {
397    let binding = get_tracker();
398    let mut tracker = binding.lock().expect("lock should not be poisoned");
399    tracker.set_emit_warnings(emit);
400    tracker.set_max_warnings_per_api(max_per_api);
401}
402
403/// Reset the entire deprecation tracker (primarily for testing)
404/// This clears all deprecations, warning counts, and resets configuration to defaults
405#[cfg(test)]
406pub fn reset_deprecation_tracker() {
407    let binding = get_tracker();
408    let mut tracker = binding.lock().expect("lock should not be poisoned");
409    tracker.deprecations.clear();
410    tracker.warning_counts.clear();
411    tracker.max_warnings_per_api = 10;
412    tracker.emit_warnings = false; // Safe default for tests
413}
414
415/// Generate a deprecation report
416pub struct DeprecationReport {
417    /// Deprecations that are currently active (should warn)
418    pub active: Vec<DeprecationInfo>,
419    /// Deprecations that have been removed
420    pub removed: Vec<DeprecationInfo>,
421    /// Deprecations pending (not yet deprecated)
422    pub pending: Vec<DeprecationInfo>,
423    /// Warning statistics
424    pub warning_stats: HashMap<String, usize>,
425}
426
427impl DeprecationReport {
428    /// Generate a deprecation report
429    pub fn generate() -> Self {
430        let binding = get_tracker();
431        let tracker = binding.lock().expect("lock should not be poisoned");
432        let all_deprecations = tracker.get_all_deprecations();
433
434        let mut active = Vec::new();
435        let mut removed = Vec::new();
436        let mut pending = Vec::new();
437
438        for info in all_deprecations {
439            if info.is_removed() {
440                removed.push(info);
441            } else if info.should_warn() {
442                active.push(info);
443            } else {
444                pending.push(info);
445            }
446        }
447
448        Self {
449            active,
450            removed,
451            pending,
452            warning_stats: tracker.get_warning_stats(),
453        }
454    }
455
456    /// Format the report as a string
457    pub fn format(&self) -> String {
458        let mut report = String::from("ToRSh API Deprecation Report\n");
459        report.push_str("==============================\n\n");
460
461        report.push_str(&format!("Current Version: {}\n\n", TORSH_VERSION));
462
463        if !self.active.is_empty() {
464            report.push_str(&format!("Active Deprecations ({})\n", self.active.len()));
465            report.push_str("-------------------------\n");
466            for info in &self.active {
467                report.push_str(&format!(
468                    "  • {} (deprecated in {}, removed in {})\n",
469                    info.api_name, info.deprecated_in, info.removed_in
470                ));
471                if let Some(ref repl) = info.replacement {
472                    report.push_str(&format!("    Replacement: {}\n", repl));
473                }
474                if let Some(count) = self.warning_stats.get(&info.api_name) {
475                    report.push_str(&format!("    Warnings emitted: {}\n", count));
476                }
477            }
478            report.push('\n');
479        }
480
481        if !self.removed.is_empty() {
482            report.push_str(&format!("Removed APIs ({})\n", self.removed.len()));
483            report.push_str("-----------------\n");
484            for info in &self.removed {
485                report.push_str(&format!(
486                    "  • {} (removed in {})\n",
487                    info.api_name, info.removed_in
488                ));
489            }
490            report.push('\n');
491        }
492
493        if !self.pending.is_empty() {
494            report.push_str(&format!("Pending Deprecations ({})\n", self.pending.len()));
495            report.push_str("-------------------------\n");
496            for info in &self.pending {
497                report.push_str(&format!(
498                    "  • {} (will be deprecated in {})\n",
499                    info.api_name, info.deprecated_in
500                ));
501            }
502            report.push('\n');
503        }
504
505        report
506    }
507}
508
509/// Macro to mark a function as deprecated
510#[macro_export]
511macro_rules! deprecated {
512    ($name:expr, $deprecated_in:expr, $removed_in:expr) => {
513        $crate::api_compat::deprecation_warning_inline($name, $deprecated_in, $removed_in, None);
514    };
515    ($name:expr, $deprecated_in:expr, $removed_in:expr, $replacement:expr) => {
516        $crate::api_compat::deprecation_warning_inline(
517            $name,
518            $deprecated_in,
519            $removed_in,
520            Some($replacement),
521        );
522    };
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_version_parsing() {
531        let v = Version::parse("1.2.3").expect("parse should succeed");
532        assert_eq!(v, Version::new(1, 2, 3));
533
534        assert!(Version::parse("invalid").is_none());
535        assert!(Version::parse("1.2").is_none());
536    }
537
538    #[test]
539    fn test_version_compatibility() {
540        let v1 = Version::new(1, 2, 3);
541        let v2 = Version::new(1, 3, 0);
542        let v3 = Version::new(2, 0, 0);
543
544        assert!(v1.is_compatible_with(&v2));
545        assert!(!v1.is_compatible_with(&v3));
546
547        // 0.x.y versions require minor match
548        let v4 = Version::new(0, 1, 0);
549        let v5 = Version::new(0, 1, 5);
550        let v6 = Version::new(0, 2, 0);
551
552        assert!(v4.is_compatible_with(&v5));
553        assert!(!v4.is_compatible_with(&v6));
554    }
555
556    #[test]
557    fn test_deprecation_info() {
558        let info = DeprecationInfo::new("old_func", Version::new(0, 1, 0), Version::new(0, 2, 0))
559            .with_replacement("new_func")
560            .with_reason("Better performance");
561
562        assert_eq!(info.api_name, "old_func");
563        assert_eq!(info.replacement, Some("new_func".to_string()));
564        assert_eq!(info.reason, Some("Better performance".to_string()));
565    }
566
567    #[test]
568    fn test_deprecation_registration() {
569        clear_deprecation_counts();
570
571        let info = DeprecationInfo::new("test_api", Version::new(0, 0, 1), Version::new(1, 0, 0))
572            .with_replacement("new_test_api");
573
574        register_deprecation(info);
575
576        let retrieved =
577            get_deprecation_info("test_api").expect("get_deprecation_info should succeed");
578        assert_eq!(retrieved.api_name, "test_api");
579        assert_eq!(retrieved.replacement, Some("new_test_api".to_string()));
580    }
581
582    #[test]
583    fn test_deprecation_warning() {
584        // Enable warnings and clear counts for this test
585        configure_deprecation_warnings(true, 10);
586        clear_deprecation_counts();
587
588        let info = DeprecationInfo::new(
589            "test_warning_api",
590            Version::new(0, 0, 1),
591            Version::new(1, 0, 0),
592        );
593
594        register_deprecation(info);
595        deprecation_warning("test_warning_api");
596
597        let stats = get_deprecation_stats();
598        // The warning should have been recorded at least once
599        let count = stats.get("test_warning_api").copied().unwrap_or(0);
600        assert!(count >= 1, "Expected at least 1 warning, got {}", count);
601    }
602
603    #[test]
604    fn test_deprecation_report() {
605        clear_deprecation_counts();
606
607        // Register some test deprecations
608        register_deprecation(DeprecationInfo::new(
609            "active_api",
610            Version::new(0, 0, 1),
611            Version::new(1, 0, 0),
612        ));
613
614        let report = DeprecationReport::generate();
615        let formatted = report.format();
616
617        assert!(formatted.contains("ToRSh API Deprecation Report"));
618    }
619
620    #[test]
621    fn test_max_warnings_limit() {
622        // Use a unique API name to avoid test isolation issues
623        let unique_api = "limited_api_max_warnings_test_unique_v2";
624
625        // Reset tracker completely first to avoid interference from other tests
626        reset_deprecation_tracker();
627
628        // Now configure with our desired settings
629        configure_deprecation_warnings(false, 3);
630
631        let info = DeprecationInfo::new(unique_api, Version::new(0, 0, 1), Version::new(1, 0, 0));
632
633        register_deprecation(info);
634
635        // Track actual warnings emitted (return value indicates if warning was actually counted)
636        let mut actual_emitted = 0;
637        for _ in 0..5 {
638            if deprecation_warning(unique_api) {
639                actual_emitted += 1;
640            }
641        }
642
643        // Verify return values indicate warnings were limited
644        assert_eq!(
645            actual_emitted, 3,
646            "Should have returned true for exactly 3 warnings"
647        );
648
649        let stats = get_deprecation_stats();
650        assert_eq!(stats.get(unique_api), Some(&3)); // Should be limited to 3
651    }
652}