Skip to main content

plotlars_core/
policy.rs

1use std::sync::OnceLock;
2
3/// Controls how unsupported styling options are reported during rendering.
4///
5/// Some backends cannot express all styling options available in the IR.
6/// This policy determines the behavior when such options are encountered.
7///
8/// # Example
9///
10/// ```
11/// use plotlars::UnsupportedOptionPolicy;
12/// use plotlars::set_unsupported_option_policy;
13///
14/// set_unsupported_option_policy(UnsupportedOptionPolicy::Strict);
15/// ```
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum UnsupportedOptionPolicy {
18    /// Silently ignore unsupported options. Plot renders, nothing printed.
19    Ignore,
20    /// Print to stderr. Always visible, including in Jupyter notebooks.
21    /// This is the default.
22    Warn,
23    /// Print to stderr AND panic after listing all unsupported options.
24    Strict,
25}
26
27static POLICY: OnceLock<UnsupportedOptionPolicy> = OnceLock::new();
28
29/// Set the global unsupported option policy. First call wins; subsequent
30/// calls are ignored. If never called, the default is `Warn`.
31pub fn set_unsupported_option_policy(policy: UnsupportedOptionPolicy) {
32    let _ = POLICY.set(policy);
33}
34
35/// Get the current policy. Returns `Warn` if never explicitly set.
36pub fn unsupported_option_policy() -> UnsupportedOptionPolicy {
37    POLICY
38        .get()
39        .copied()
40        .unwrap_or(UnsupportedOptionPolicy::Warn)
41}
42
43/// Report an unsupported option. Appends to `collector` for Strict mode
44/// batch reporting. Deduplicates within the collector.
45///
46/// Called by backend converters when an IR field has no backend equivalent.
47pub fn report_unsupported(
48    backend: &str,
49    plot_type: &str,
50    option: &str,
51    collector: &mut Vec<String>,
52) {
53    let key = format!("{plot_type}.{option}");
54    if collector.contains(&key) {
55        return;
56    }
57
58    let policy = unsupported_option_policy();
59    match policy {
60        UnsupportedOptionPolicy::Ignore => {}
61        UnsupportedOptionPolicy::Warn | UnsupportedOptionPolicy::Strict => {
62            eprintln!("{backend}: `{option}` on {plot_type} not supported; ignored");
63        }
64    }
65    collector.push(key);
66}
67
68/// Call after all traces/layout are converted in Strict mode.
69/// Panics if any unsupported options were collected.
70pub fn enforce_strict(backend: &str, collector: &[String]) {
71    if unsupported_option_policy() == UnsupportedOptionPolicy::Strict && !collector.is_empty() {
72        panic!(
73            "{backend}: unsupported options encountered in Strict mode:\n  - {}",
74            collector.join("\n  - ")
75        );
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_default_policy_is_warn() {
85        // OnceLock is process-global so we can only test the default
86        // before any test calls set_unsupported_option_policy.
87        assert_eq!(unsupported_option_policy(), UnsupportedOptionPolicy::Warn);
88    }
89
90    #[test]
91    fn test_report_unsupported_deduplicates() {
92        let mut collector = Vec::new();
93        report_unsupported("test", "Scatter", "fill", &mut collector);
94        report_unsupported("test", "Scatter", "fill", &mut collector);
95        assert_eq!(collector.len(), 1);
96    }
97
98    #[test]
99    fn test_report_unsupported_different_options() {
100        let mut collector = Vec::new();
101        report_unsupported("test", "Scatter", "fill", &mut collector);
102        report_unsupported("test", "Scatter", "hover", &mut collector);
103        assert_eq!(collector.len(), 2);
104    }
105
106    #[test]
107    fn test_enforce_strict_with_empty_collector() {
108        // Should not panic
109        enforce_strict("test", &[]);
110    }
111}