agent_chain_core/api/
deprecation.rs

1//! Helper functions for deprecating parts of the Agent Chain API.
2//!
3//! This module was adapted from matplotlib's _api/deprecation.py module:
4//! https://github.com/matplotlib/matplotlib/blob/main/lib/matplotlib/_api/deprecation.py
5//!
6//! **Warning:** This module is for internal use only. Do not use it in your own code.
7//! We may change the API at any time with no warning.
8
9use std::fmt;
10use std::sync::atomic::{AtomicBool, Ordering};
11
12use super::internal::is_caller_internal;
13
14/// A warning type for deprecated features in Agent Chain.
15#[derive(Debug, Clone)]
16pub struct AgentChainDeprecationWarning {
17    message: String,
18}
19
20impl AgentChainDeprecationWarning {
21    /// Create a new deprecation warning with the given message.
22    pub fn new(message: impl Into<String>) -> Self {
23        Self {
24            message: message.into(),
25        }
26    }
27
28    /// Get the warning message.
29    pub fn message(&self) -> &str {
30        &self.message
31    }
32}
33
34impl fmt::Display for AgentChainDeprecationWarning {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "{}", self.message)
37    }
38}
39
40impl std::error::Error for AgentChainDeprecationWarning {}
41
42/// A warning type for pending deprecations in Agent Chain.
43#[derive(Debug, Clone)]
44pub struct AgentChainPendingDeprecationWarning {
45    message: String,
46}
47
48impl AgentChainPendingDeprecationWarning {
49    /// Create a new pending deprecation warning with the given message.
50    pub fn new(message: impl Into<String>) -> Self {
51        Self {
52            message: message.into(),
53        }
54    }
55
56    /// Get the warning message.
57    pub fn message(&self) -> &str {
58        &self.message
59    }
60}
61
62impl fmt::Display for AgentChainPendingDeprecationWarning {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(f, "{}", self.message)
65    }
66}
67
68impl std::error::Error for AgentChainPendingDeprecationWarning {}
69
70/// Global flag to suppress deprecation warnings.
71static SUPPRESS_DEPRECATION_WARNINGS: AtomicBool = AtomicBool::new(false);
72
73/// Parameters for configuring deprecation warnings.
74#[derive(Debug, Clone, Default)]
75pub struct DeprecationParams {
76    /// The release at which this API became deprecated.
77    pub since: String,
78    /// Override the default deprecation message.
79    pub message: Option<String>,
80    /// The name of the deprecated object.
81    pub name: Option<String>,
82    /// An alternative API that the user may use in place of the deprecated API.
83    pub alternative: Option<String>,
84    /// An alternative import path that the user may use instead.
85    pub alternative_import: Option<String>,
86    /// If `true`, uses a pending deprecation warning instead.
87    pub pending: bool,
88    /// The object type being deprecated (e.g., "function", "class", "method").
89    pub obj_type: Option<String>,
90    /// Additional text appended directly to the final message.
91    pub addendum: Option<String>,
92    /// The expected removal version.
93    pub removal: Option<String>,
94    /// The package of the deprecated object.
95    pub package: Option<String>,
96}
97
98impl DeprecationParams {
99    /// Create new deprecation parameters with the version when deprecation started.
100    pub fn new(since: impl Into<String>) -> Self {
101        Self {
102            since: since.into(),
103            ..Default::default()
104        }
105    }
106
107    /// Set the name of the deprecated item.
108    pub fn with_name(mut self, name: impl Into<String>) -> Self {
109        self.name = Some(name.into());
110        self
111    }
112
113    /// Set a custom deprecation message.
114    pub fn with_message(mut self, message: impl Into<String>) -> Self {
115        self.message = Some(message.into());
116        self
117    }
118
119    /// Set the alternative to use instead of the deprecated item.
120    pub fn with_alternative(mut self, alternative: impl Into<String>) -> Self {
121        self.alternative = Some(alternative.into());
122        self
123    }
124
125    /// Set the alternative import path.
126    pub fn with_alternative_import(mut self, alternative_import: impl Into<String>) -> Self {
127        self.alternative_import = Some(alternative_import.into());
128        self
129    }
130
131    /// Mark this as a pending deprecation.
132    pub fn with_pending(mut self, pending: bool) -> Self {
133        self.pending = pending;
134        self
135    }
136
137    /// Set the object type.
138    pub fn with_obj_type(mut self, obj_type: impl Into<String>) -> Self {
139        self.obj_type = Some(obj_type.into());
140        self
141    }
142
143    /// Set the addendum text.
144    pub fn with_addendum(mut self, addendum: impl Into<String>) -> Self {
145        self.addendum = Some(addendum.into());
146        self
147    }
148
149    /// Set the expected removal version.
150    pub fn with_removal(mut self, removal: impl Into<String>) -> Self {
151        self.removal = Some(removal.into());
152        self
153    }
154
155    /// Set the package name.
156    pub fn with_package(mut self, package: impl Into<String>) -> Self {
157        self.package = Some(package.into());
158        self
159    }
160
161    /// Validate the deprecation parameters.
162    ///
163    /// Returns an error if the parameters are invalid.
164    pub fn validate(&self) -> Result<(), String> {
165        if self.pending && self.removal.is_some() {
166            return Err("A pending deprecation cannot have a scheduled removal".to_string());
167        }
168        // Non-pending deprecations must have a removal version specified
169        // This matches Python's NotImplementedError behavior
170        if !self.pending && self.removal.is_none() && self.message.is_none() {
171            return Err(
172                "Need to determine which default deprecation schedule to use. \
173                 Non-pending deprecations must specify a removal version."
174                    .to_string(),
175            );
176        }
177        if self.alternative.is_some() && self.alternative_import.is_some() {
178            return Err("Cannot specify both alternative and alternative_import".to_string());
179        }
180        if let Some(ref alt_import) = self.alternative_import
181            && !alt_import.contains("::")
182        {
183            return Err(format!(
184                "alternative_import must be a fully qualified module path. Got {}",
185                alt_import
186            ));
187        }
188        Ok(())
189    }
190}
191
192/// Parameters for renaming a deprecated parameter.
193#[derive(Debug, Clone)]
194pub struct RenameParameterParams {
195    /// The version in which the parameter was renamed.
196    pub since: String,
197    /// The version in which the old parameter will be removed.
198    pub removal: String,
199    /// The old parameter name.
200    pub old: String,
201    /// The new parameter name.
202    pub new: String,
203}
204
205impl RenameParameterParams {
206    /// Create new rename parameter params.
207    pub fn new(
208        since: impl Into<String>,
209        removal: impl Into<String>,
210        old: impl Into<String>,
211        new: impl Into<String>,
212    ) -> Self {
213        Self {
214            since: since.into(),
215            removal: removal.into(),
216            old: old.into(),
217            new: new.into(),
218        }
219    }
220}
221
222/// Check if an old parameter name was used and emit a deprecation warning.
223///
224/// This function is used to handle parameter renaming with deprecation warnings.
225/// It checks if the old parameter name is present in the provided parameters,
226/// and if so, emits a deprecation warning and returns the value that should be used.
227///
228/// # Arguments
229///
230/// * `params` - The rename parameter configuration.
231/// * `old_value` - The value passed with the old parameter name (if any).
232/// * `new_value` - The value passed with the new parameter name (if any).
233/// * `func_name` - The name of the function for the warning message.
234/// * `caller_module` - The module path of the caller.
235///
236/// # Returns
237///
238/// * `Ok(value)` - The value to use (old_value takes precedence if both are provided).
239/// * `Err(message)` - If both old and new parameters were provided.
240///
241/// # Example
242///
243/// ```ignore
244/// use agent_chain_core::api::{handle_renamed_parameter, RenameParameterParams};
245///
246/// fn my_function(new_param: Option<String>, old_param: Option<String>) -> Result<(), String> {
247///     let params = RenameParameterParams::new("0.1.0", "0.2.0", "old_param", "new_param");
248///     let value = handle_renamed_parameter(
249///         &params,
250///         old_param,
251///         new_param,
252///         "my_function",
253///         module_path!()
254///     )?;
255///     // Use `value` which is the resolved parameter value
256///     Ok(())
257/// }
258/// ```
259pub fn handle_renamed_parameter<T>(
260    params: &RenameParameterParams,
261    old_value: Option<T>,
262    new_value: Option<T>,
263    func_name: &str,
264    caller_module: &str,
265) -> Result<Option<T>, String> {
266    match (old_value, new_value) {
267        (Some(_), Some(_)) => Err(format!(
268            "{}() got multiple values for argument '{}'",
269            func_name, params.new
270        )),
271        (Some(old), None) => {
272            // Emit deprecation warning for using old parameter
273            warn_deprecated(
274                DeprecationParams::new(&params.since)
275                    .with_message(format!(
276                        "The parameter `{}` of `{}` was deprecated in {} and will be removed in {}. Use `{}` instead.",
277                        params.old, func_name, params.since, params.removal, params.new
278                    ))
279                    .with_removal(&params.removal),
280                caller_module,
281            );
282            Ok(Some(old))
283        }
284        (None, new) => Ok(new),
285    }
286}
287
288/// Display a standardized deprecation warning.
289///
290/// # Arguments
291///
292/// * `params` - Parameters for the deprecation warning.
293/// * `caller_module` - The module path of the caller (typically from `module_path!()` macro).
294///
295/// # Example
296///
297/// ```
298/// use agent_chain_core::api::{warn_deprecated, DeprecationParams};
299///
300/// // Simple deprecation warning
301/// warn_deprecated(
302///     DeprecationParams::new("0.1.0")
303///         .with_name("old_function")
304///         .with_removal("0.2.0"),
305///     module_path!()
306/// );
307///
308/// // With alternative
309/// warn_deprecated(
310///     DeprecationParams::new("0.1.0")
311///         .with_name("OldClass")
312///         .with_obj_type("class")
313///         .with_alternative("NewClass")
314///         .with_removal("0.2.0"),
315///     module_path!()
316/// );
317/// ```
318pub fn warn_deprecated(params: DeprecationParams, caller_module: &str) {
319    // Skip if warnings are suppressed
320    if SUPPRESS_DEPRECATION_WARNINGS.load(Ordering::Relaxed) {
321        return;
322    }
323
324    // Skip if caller is internal
325    if is_caller_internal(caller_module) {
326        return;
327    }
328
329    // Validate parameters
330    if let Err(err) = params.validate() {
331        eprintln!("Invalid deprecation parameters: {}", err);
332        return;
333    }
334
335    let message = if let Some(msg) = params.message {
336        msg
337    } else {
338        let name = params.name.unwrap_or_else(|| "unknown".to_string());
339        let package = params.package.unwrap_or_else(|| "agent-chain".to_string());
340
341        let mut msg = if let Some(ref obj_type) = params.obj_type {
342            format!("The {} `{}`", obj_type, name)
343        } else {
344            format!("`{}`", name)
345        };
346
347        if params.pending {
348            msg.push_str(" will be deprecated in a future version");
349        } else {
350            msg.push_str(&format!(" was deprecated in {} {}", package, params.since));
351
352            if let Some(ref removal) = params.removal {
353                msg.push_str(&format!(" and will be removed in {}", removal));
354            }
355        }
356
357        if let Some(ref alternative_import) = params.alternative_import {
358            let alt_package = alternative_import
359                .split("::")
360                .next()
361                .unwrap_or(alternative_import)
362                .replace('_', "-");
363
364            if alt_package == package {
365                msg.push_str(&format!(". Use {} instead.", alternative_import));
366            } else {
367                let parts: Vec<&str> = alternative_import.rsplitn(2, "::").collect();
368                if parts.len() == 2 {
369                    let alt_name = parts[0];
370                    let alt_module = parts[1];
371                    msg.push_str(&format!(
372                        ". An updated version of the {} exists in the {} package and should be used instead. \
373                         To use it add `{}` to your dependencies and import as `use {}::{};`.",
374                        params.obj_type.as_deref().unwrap_or("item"),
375                        alt_package,
376                        alt_package,
377                        alt_module,
378                        alt_name
379                    ));
380                }
381            }
382        } else if let Some(ref alternative) = params.alternative {
383            msg.push_str(&format!(". Use {} instead.", alternative));
384        }
385
386        if let Some(ref addendum) = params.addendum {
387            msg.push(' ');
388            msg.push_str(addendum);
389        }
390
391        msg
392    };
393
394    if params.pending {
395        let warning = AgentChainPendingDeprecationWarning::new(message);
396        eprintln!("AgentChainPendingDeprecationWarning: {}", warning);
397    } else {
398        let warning = AgentChainDeprecationWarning::new(message);
399        eprintln!("AgentChainDeprecationWarning: {}", warning);
400    }
401}
402
403/// Guard that suppresses deprecation warnings while it exists.
404pub struct SuppressDeprecationWarnings {
405    previous_state: bool,
406}
407
408impl SuppressDeprecationWarnings {
409    /// Create a new guard that suppresses deprecation warnings.
410    pub fn new() -> Self {
411        let previous_state = SUPPRESS_DEPRECATION_WARNINGS.swap(true, Ordering::Relaxed);
412        Self { previous_state }
413    }
414}
415
416impl Default for SuppressDeprecationWarnings {
417    fn default() -> Self {
418        Self::new()
419    }
420}
421
422impl Drop for SuppressDeprecationWarnings {
423    fn drop(&mut self) {
424        SUPPRESS_DEPRECATION_WARNINGS.store(self.previous_state, Ordering::Relaxed);
425    }
426}
427
428/// Suppress deprecation warnings within a scope.
429///
430/// # Example
431///
432/// ```
433/// use agent_chain_core::api::suppress_deprecation_warnings;
434///
435/// {
436///     let _guard = suppress_deprecation_warnings();
437///     // Deprecation warnings are suppressed here
438/// }
439/// // Deprecation warnings are restored here
440/// ```
441pub fn suppress_deprecation_warnings() -> SuppressDeprecationWarnings {
442    SuppressDeprecationWarnings::new()
443}
444
445/// Enable deprecation warnings (unmute them).
446///
447/// This function enables deprecation warnings that may have been suppressed.
448pub fn surface_deprecation_warnings() {
449    SUPPRESS_DEPRECATION_WARNINGS.store(false, Ordering::Relaxed);
450}
451
452/// Macro for handling renamed parameters with deprecation warnings.
453///
454/// This macro simplifies the common pattern of renaming a function parameter
455/// while maintaining backward compatibility.
456///
457/// # Example
458///
459/// ```ignore
460/// use agent_chain_core::renamed_parameter;
461///
462/// fn my_function(new_param: Option<String>, old_param: Option<String>) -> Result<String, String> {
463///     let value = renamed_parameter!(
464///         since = "0.1.0",
465///         removal = "0.2.0",
466///         old = "old_param" => old_param,
467///         new = "new_param" => new_param,
468///         func = "my_function"
469///     )?;
470///     Ok(value.unwrap_or_default())
471/// }
472/// ```
473#[macro_export]
474macro_rules! renamed_parameter {
475    (
476        since = $since:expr,
477        removal = $removal:expr,
478        old = $old_name:expr => $old_value:expr,
479        new = $new_name:expr => $new_value:expr,
480        func = $func_name:expr
481    ) => {{
482        let params =
483            $crate::api::RenameParameterParams::new($since, $removal, $old_name, $new_name);
484        $crate::api::handle_renamed_parameter(
485            &params,
486            $old_value,
487            $new_value,
488            $func_name,
489            module_path!(),
490        )
491    }};
492}
493
494/// Macro to emit a deprecation warning.
495///
496/// # Example
497///
498/// ```ignore
499/// use agent_chain_core::deprecated;
500///
501/// // Simple deprecation
502/// deprecated!("0.1.0", "old_function", removal = "0.2.0");
503///
504/// // With alternative
505/// deprecated!("0.1.0", "OldClass",
506///     obj_type = "class",
507///     alternative = "NewClass",
508///     removal = "0.2.0"
509/// );
510/// ```
511#[macro_export]
512macro_rules! deprecated {
513    ($since:expr, $name:expr $(, $key:ident = $value:expr)* $(,)?) => {{
514        let mut params = $crate::api::DeprecationParams::new($since).with_name($name);
515        $(
516            params = $crate::deprecated!(@set params, $key, $value);
517        )*
518        $crate::api::warn_deprecated(params, module_path!())
519    }};
520    (@set $params:expr, message, $value:expr) => {
521        $params.with_message($value)
522    };
523    (@set $params:expr, alternative, $value:expr) => {
524        $params.with_alternative($value)
525    };
526    (@set $params:expr, alternative_import, $value:expr) => {
527        $params.with_alternative_import($value)
528    };
529    (@set $params:expr, pending, $value:expr) => {
530        $params.with_pending($value)
531    };
532    (@set $params:expr, obj_type, $value:expr) => {
533        $params.with_obj_type($value)
534    };
535    (@set $params:expr, addendum, $value:expr) => {
536        $params.with_addendum($value)
537    };
538    (@set $params:expr, removal, $value:expr) => {
539        $params.with_removal($value)
540    };
541    (@set $params:expr, package, $value:expr) => {
542        $params.with_package($value)
543    };
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn test_deprecation_warning_creation() {
552        let warning = AgentChainDeprecationWarning::new("Test warning");
553        assert_eq!(warning.message(), "Test warning");
554        assert_eq!(format!("{}", warning), "Test warning");
555    }
556
557    #[test]
558    fn test_pending_deprecation_warning_creation() {
559        let warning = AgentChainPendingDeprecationWarning::new("Test pending warning");
560        assert_eq!(warning.message(), "Test pending warning");
561        assert_eq!(format!("{}", warning), "Test pending warning");
562    }
563
564    #[test]
565    fn test_deprecation_params_builder() {
566        let params = DeprecationParams::new("0.1.0")
567            .with_name("test_function")
568            .with_obj_type("function")
569            .with_alternative("new_function")
570            .with_removal("0.2.0");
571
572        assert_eq!(params.since, "0.1.0");
573        assert_eq!(params.name, Some("test_function".to_string()));
574        assert_eq!(params.obj_type, Some("function".to_string()));
575        assert_eq!(params.alternative, Some("new_function".to_string()));
576        assert_eq!(params.removal, Some("0.2.0".to_string()));
577    }
578
579    #[test]
580    fn test_deprecation_params_validation() {
581        // Valid params with removal
582        let params = DeprecationParams::new("0.1.0")
583            .with_name("test")
584            .with_removal("0.2.0");
585        assert!(params.validate().is_ok());
586
587        // Valid pending params without removal
588        let params = DeprecationParams::new("0.1.0")
589            .with_name("test")
590            .with_pending(true);
591        assert!(params.validate().is_ok());
592
593        // Valid params with custom message (doesn't require removal)
594        let params = DeprecationParams::new("0.1.0")
595            .with_name("test")
596            .with_message("Custom deprecation message");
597        assert!(params.validate().is_ok());
598
599        // Pending with removal is invalid
600        let params = DeprecationParams::new("0.1.0")
601            .with_pending(true)
602            .with_removal("0.2.0");
603        assert!(params.validate().is_err());
604
605        // Non-pending without removal is invalid (matches Python's NotImplementedError)
606        let params = DeprecationParams::new("0.1.0").with_name("test");
607        assert!(params.validate().is_err());
608
609        // Both alternative and alternative_import is invalid
610        let params = DeprecationParams::new("0.1.0")
611            .with_alternative("new_thing")
612            .with_alternative_import("some::path::NewThing")
613            .with_removal("0.2.0");
614        assert!(params.validate().is_err());
615
616        // alternative_import without :: is invalid
617        let params = DeprecationParams::new("0.1.0")
618            .with_alternative_import("InvalidPath")
619            .with_removal("0.2.0");
620        assert!(params.validate().is_err());
621    }
622
623    #[test]
624    fn test_suppress_deprecation_warnings() {
625        // Ensure warnings are not suppressed initially
626        surface_deprecation_warnings();
627        assert!(!SUPPRESS_DEPRECATION_WARNINGS.load(Ordering::Relaxed));
628
629        {
630            let _guard = suppress_deprecation_warnings();
631            assert!(SUPPRESS_DEPRECATION_WARNINGS.load(Ordering::Relaxed));
632        }
633
634        assert!(!SUPPRESS_DEPRECATION_WARNINGS.load(Ordering::Relaxed));
635    }
636
637    #[test]
638    fn test_rename_parameter_params() {
639        let params = RenameParameterParams::new("0.1.0", "0.2.0", "old_name", "new_name");
640        assert_eq!(params.since, "0.1.0");
641        assert_eq!(params.removal, "0.2.0");
642        assert_eq!(params.old, "old_name");
643        assert_eq!(params.new, "new_name");
644    }
645
646    #[test]
647    fn test_handle_renamed_parameter_new_only() {
648        surface_deprecation_warnings();
649        let params = RenameParameterParams::new("0.1.0", "0.2.0", "old_param", "new_param");
650
651        // Only new parameter provided - should return the new value
652        let result = handle_renamed_parameter(
653            &params,
654            None::<String>,
655            Some("new_value".to_string()),
656            "test_func",
657            "external_crate::module",
658        );
659        assert!(result.is_ok());
660        assert_eq!(result.unwrap(), Some("new_value".to_string()));
661    }
662
663    #[test]
664    fn test_handle_renamed_parameter_old_only() {
665        surface_deprecation_warnings();
666        let params = RenameParameterParams::new("0.1.0", "0.2.0", "old_param", "new_param");
667
668        // Only old parameter provided - should return the old value (with warning)
669        let result = handle_renamed_parameter(
670            &params,
671            Some("old_value".to_string()),
672            None,
673            "test_func",
674            "external_crate::module",
675        );
676        assert!(result.is_ok());
677        assert_eq!(result.unwrap(), Some("old_value".to_string()));
678    }
679
680    #[test]
681    fn test_handle_renamed_parameter_both_provided() {
682        let params = RenameParameterParams::new("0.1.0", "0.2.0", "old_param", "new_param");
683
684        // Both parameters provided - should return error
685        let result = handle_renamed_parameter(
686            &params,
687            Some("old_value".to_string()),
688            Some("new_value".to_string()),
689            "test_func",
690            "external_crate::module",
691        );
692        assert!(result.is_err());
693        assert!(
694            result
695                .unwrap_err()
696                .contains("got multiple values for argument")
697        );
698    }
699
700    #[test]
701    fn test_handle_renamed_parameter_none() {
702        let params = RenameParameterParams::new("0.1.0", "0.2.0", "old_param", "new_param");
703
704        // Neither parameter provided - should return None
705        let result = handle_renamed_parameter(
706            &params,
707            None::<String>,
708            None,
709            "test_func",
710            "external_crate::module",
711        );
712        assert!(result.is_ok());
713        assert_eq!(result.unwrap(), None);
714    }
715}