use crate::error_reporter::{Severity, global_reporter};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde_json::Value;
use std::collections::HashMap;
use std::panic::Location;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeprecationWarning {
pub message: String,
pub callsite: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeprecationBehavior {
Raise,
Log,
Silence,
Report,
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum DeprecationError {
#[error("deprecated API used: {0}")]
Raised(String),
}
pub struct Deprecation;
static DEPRECATION_BEHAVIOR: Lazy<Mutex<DeprecationBehavior>> =
Lazy::new(|| Mutex::new(DeprecationBehavior::Log));
static LOGGED_WARNINGS: Lazy<Mutex<Vec<DeprecationWarning>>> = Lazy::new(|| Mutex::new(Vec::new()));
impl Deprecation {
pub fn set_behavior(behavior: DeprecationBehavior) {
*DEPRECATION_BEHAVIOR.lock() = behavior;
}
#[must_use]
pub fn behavior() -> DeprecationBehavior {
*DEPRECATION_BEHAVIOR.lock()
}
#[track_caller]
pub fn warn(message: impl Into<String>) -> Result<DeprecationWarning, DeprecationError> {
let warning = DeprecationWarning {
message: message.into(),
callsite: Location::caller().to_string(),
};
match Self::behavior() {
DeprecationBehavior::Raise => Err(DeprecationError::Raised(warning.message.clone())),
DeprecationBehavior::Log => {
tracing::warn!(message = %warning.message, callsite = %warning.callsite, "deprecation warning");
LOGGED_WARNINGS.lock().push(warning.clone());
Ok(warning)
}
DeprecationBehavior::Silence => Ok(warning),
DeprecationBehavior::Report => {
let mut context = HashMap::new();
context.insert(
String::from("callsite"),
Value::String(warning.callsite.clone()),
);
global_reporter().report_with_severity(
warning.message.clone(),
true,
Severity::Warning,
context,
);
Ok(warning)
}
}
}
}
#[track_caller]
pub fn deprecated(message: impl Into<String>) -> Result<DeprecationWarning, DeprecationError> {
Deprecation::warn(message)
}
#[cfg(test)]
pub(crate) fn reset_logged_warnings() {
LOGGED_WARNINGS.lock().clear();
Deprecation::set_behavior(DeprecationBehavior::Log);
}
#[cfg(test)]
mod tests {
use super::{
Deprecation, DeprecationBehavior, DeprecationError, DeprecationWarning, deprecated,
reset_logged_warnings,
};
use crate::error_reporter::{
ErrorReport, ErrorSubscriber, global_reporter, reset_global_reporter, subscribe,
};
use parking_lot::Mutex;
use std::sync::Arc;
#[derive(Default)]
struct RecordingSubscriber {
reports: Arc<Mutex<Vec<ErrorReport>>>,
}
impl RecordingSubscriber {
fn new() -> Self {
Self::default()
}
fn handle(&self) -> Arc<Mutex<Vec<ErrorReport>>> {
Arc::clone(&self.reports)
}
}
impl ErrorSubscriber for RecordingSubscriber {
fn report(&self, report: &ErrorReport) {
self.reports.lock().push(report.clone());
}
}
fn logged_warnings() -> Vec<DeprecationWarning> {
super::LOGGED_WARNINGS.lock().clone()
}
#[test]
fn default_behavior_is_log() {
reset_logged_warnings();
assert_eq!(Deprecation::behavior(), DeprecationBehavior::Log);
}
#[test]
fn set_behavior_updates_the_global_behavior() {
reset_logged_warnings();
Deprecation::set_behavior(DeprecationBehavior::Silence);
assert_eq!(Deprecation::behavior(), DeprecationBehavior::Silence);
}
#[test]
fn warn_logs_by_default() {
reset_logged_warnings();
let warning = Deprecation::warn("old API").expect("log behavior should not fail");
assert_eq!(warning.message, "old API");
assert_eq!(logged_warnings().len(), 1);
}
#[test]
fn warn_records_the_callsite() {
reset_logged_warnings();
let warning = Deprecation::warn("old API").expect("log behavior should not fail");
assert!(warning.callsite.contains("deprecation.rs"));
}
#[test]
fn silence_behavior_does_not_log() {
reset_logged_warnings();
Deprecation::set_behavior(DeprecationBehavior::Silence);
let warning = Deprecation::warn("old API").expect("silenced warnings should still return");
assert_eq!(warning.message, "old API");
assert!(logged_warnings().is_empty());
}
#[test]
fn raise_behavior_returns_a_typed_error() {
reset_logged_warnings();
Deprecation::set_behavior(DeprecationBehavior::Raise);
let error = Deprecation::warn("old API").expect_err("raise behavior should error");
assert_eq!(error, DeprecationError::Raised(String::from("old API")));
}
#[test]
fn raise_behavior_does_not_log() {
reset_logged_warnings();
Deprecation::set_behavior(DeprecationBehavior::Raise);
let _ = Deprecation::warn("old API");
assert!(logged_warnings().is_empty());
}
#[test]
fn report_behavior_forwards_to_the_global_error_reporter() {
reset_logged_warnings();
reset_global_reporter();
Deprecation::set_behavior(DeprecationBehavior::Report);
let subscriber = RecordingSubscriber::new();
let reports = subscriber.handle();
subscribe(subscriber);
Deprecation::warn("old API").expect("report behavior should not fail");
let reports = reports.lock();
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].error, "old API");
assert!(
reports[0]
.context
.get("callsite")
.and_then(|value| value.as_str())
.is_some()
);
}
#[test]
fn report_behavior_does_not_add_logged_warnings() {
reset_logged_warnings();
reset_global_reporter();
Deprecation::set_behavior(DeprecationBehavior::Report);
Deprecation::warn("old API").expect("report behavior should not fail");
assert!(logged_warnings().is_empty());
}
#[test]
fn helper_function_uses_the_same_behavior() {
reset_logged_warnings();
let warning =
deprecated("legacy helper").expect("helper should forward to deprecation warn");
assert_eq!(warning.message, "legacy helper");
assert_eq!(logged_warnings().len(), 1);
}
#[test]
fn global_error_reporter_remains_accessible_after_reporting() {
reset_logged_warnings();
reset_global_reporter();
Deprecation::set_behavior(DeprecationBehavior::Report);
let reporter = global_reporter() as *const _;
Deprecation::warn("old API").expect("report behavior should not fail");
assert_eq!(reporter, global_reporter() as *const _);
}
}