wezterm_dynamic/
error.rs

1use crate::fromdynamic::{FromDynamicOptions, UnknownFieldAction};
2use crate::object::Object;
3use crate::value::Value;
4use std::cell::RefCell;
5use std::rc::Rc;
6use thiserror::Error;
7
8pub trait WarningCollector {
9    fn warn(&self, message: String);
10}
11
12thread_local! {
13    static WARNING_COLLECTOR: RefCell<Option<Box<dyn WarningCollector>>> = RefCell::new(None);
14}
15
16#[derive(Error, Debug)]
17#[non_exhaustive]
18pub enum Error {
19    #[error("`{}` is not a valid {} variant. {}", .variant_name, .type_name, Self::possible_matches(.variant_name, &.possible))]
20    InvalidVariantForType {
21        variant_name: String,
22        type_name: &'static str,
23        possible: &'static [&'static str],
24    },
25    #[error("`{}` is not a valid {} field. {}", .field_name, .type_name, Self::possible_matches(.field_name, &.possible))]
26    UnknownFieldForStruct {
27        field_name: String,
28        type_name: &'static str,
29        possible: &'static [&'static str],
30    },
31    #[error("{}", .0)]
32    Message(String),
33    #[error("Cannot coerce vec of size {} to array of size {}", .vec_size, .array_size)]
34    ArraySizeMismatch { vec_size: usize, array_size: usize },
35    #[error("Cannot convert `{}` to `{}`", .source_type, .dest_type)]
36    NoConversion {
37        source_type: String,
38        dest_type: &'static str,
39    },
40    #[error("Expected char to be a string with a single character")]
41    CharFromWrongSizedString,
42    #[error("Expected a valid `{}` variant name as single key in object, but there are {} keys", .type_name, .num_keys)]
43    IncorrectNumberOfEnumKeys {
44        type_name: &'static str,
45        num_keys: usize,
46    },
47    #[error("Error processing {}::{}: {:#}", .type_name, .field_name, .error)]
48    ErrorInField {
49        type_name: &'static str,
50        field_name: &'static str,
51        error: String,
52    },
53    #[error("Error processing {} (types: {}) {:#}", .field_name.join("."), .type_name.join(", "), .error)]
54    ErrorInNestedField {
55        type_name: Vec<&'static str>,
56        field_name: Vec<&'static str>,
57        error: String,
58    },
59    #[error("`{}` is not a valid type to use as a field name in `{}`", .key_type, .type_name)]
60    InvalidFieldType {
61        type_name: &'static str,
62        key_type: String,
63    },
64    #[error("{}::{} is deprecated: {}", .type_name, .field_name, .reason)]
65    DeprecatedField {
66        type_name: &'static str,
67        field_name: &'static str,
68        reason: &'static str,
69    },
70}
71
72impl Error {
73    /// Log a warning; if a warning collector is set for the current thread,
74    /// use it, otherwise, log a regular warning message.
75    pub fn warn(message: String) {
76        WARNING_COLLECTOR.with(|collector| {
77            let collector = collector.borrow();
78            if let Some(collector) = collector.as_ref() {
79                collector.warn(message);
80            } else {
81                log::warn!("{message}");
82            }
83        });
84    }
85
86    pub fn capture_warnings<F: FnOnce() -> T, T>(f: F) -> (T, Vec<String>) {
87        let warnings = Rc::new(RefCell::new(vec![]));
88
89        struct Collector {
90            warnings: Rc<RefCell<Vec<String>>>,
91        }
92
93        impl WarningCollector for Collector {
94            fn warn(&self, message: String) {
95                self.warnings.borrow_mut().push(message);
96            }
97        }
98
99        Self::set_warning_collector(Collector {
100            warnings: Rc::clone(&warnings),
101        });
102        let result = f();
103        Self::clear_warning_collector();
104        let warnings = match Rc::try_unwrap(warnings) {
105            Ok(warnings) => warnings.into_inner(),
106            Err(warnings) => (*warnings).clone().into_inner(),
107        };
108        (result, warnings)
109    }
110
111    /// Replace the warning collector for the current thread
112    fn set_warning_collector<T: WarningCollector + 'static>(c: T) {
113        WARNING_COLLECTOR.with(|collector| {
114            collector.borrow_mut().replace(Box::new(c));
115        });
116    }
117
118    /// Clear the warning collector for the current thread
119    fn clear_warning_collector() {
120        WARNING_COLLECTOR.with(|collector| {
121            collector.borrow_mut().take();
122        });
123    }
124
125    fn compute_unknown_fields(
126        type_name: &'static str,
127        object: &crate::Object,
128        possible: &'static [&'static str],
129    ) -> Vec<Self> {
130        let mut errors = vec![];
131
132        for key in object.keys() {
133            match key {
134                Value::String(s) => {
135                    if !possible.contains(&s.as_str()) {
136                        errors.push(Self::UnknownFieldForStruct {
137                            field_name: s.to_string(),
138                            type_name,
139                            possible,
140                        });
141                    }
142                }
143                other => {
144                    errors.push(Self::InvalidFieldType {
145                        type_name,
146                        key_type: other.variant_name().to_string(),
147                    });
148                }
149            }
150        }
151
152        errors
153    }
154
155    pub fn raise_deprecated_fields(
156        options: FromDynamicOptions,
157        type_name: &'static str,
158        field_name: &'static str,
159        reason: &'static str,
160    ) -> Result<(), Self> {
161        if options.deprecated_fields == UnknownFieldAction::Ignore {
162            return Ok(());
163        }
164        let err = Self::DeprecatedField {
165            type_name,
166            field_name,
167            reason,
168        };
169
170        match options.deprecated_fields {
171            UnknownFieldAction::Deny => Err(err),
172            UnknownFieldAction::Warn => {
173                Self::warn(format!("{:#}", err));
174                Ok(())
175            }
176            UnknownFieldAction::Ignore => unreachable!(),
177        }
178    }
179
180    pub fn raise_unknown_fields(
181        options: FromDynamicOptions,
182        type_name: &'static str,
183        object: &crate::Object,
184        possible: &'static [&'static str],
185    ) -> Result<(), Self> {
186        if options.unknown_fields == UnknownFieldAction::Ignore {
187            return Ok(());
188        }
189
190        let errors = Self::compute_unknown_fields(type_name, object, possible);
191        if errors.is_empty() {
192            return Ok(());
193        }
194
195        let show_warning = options.unknown_fields == UnknownFieldAction::Warn || errors.len() > 1;
196
197        if show_warning {
198            for err in &errors {
199                Self::warn(format!("{:#}", err));
200            }
201        }
202
203        if options.unknown_fields == UnknownFieldAction::Deny {
204            for err in errors {
205                return Err(err);
206            }
207        }
208
209        Ok(())
210    }
211
212    fn possible_matches(used: &str, possible: &'static [&'static str]) -> String {
213        // Produce similar field name list
214        let mut candidates: Vec<(f64, &str)> = possible
215            .iter()
216            .map(|&name| (strsim::jaro_winkler(used, name), name))
217            .filter(|(confidence, _)| *confidence > 0.8)
218            .collect();
219        candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
220        let suggestions: Vec<&str> = candidates.into_iter().map(|(_, name)| name).collect();
221
222        // Filter the suggestions out of the allowed field names
223        // and sort what remains.
224        let mut fields: Vec<&str> = possible
225            .iter()
226            .filter(|&name| !suggestions.iter().any(|candidate| candidate == name))
227            .copied()
228            .collect();
229        fields.sort_unstable();
230
231        let mut message = String::new();
232
233        match suggestions.len() {
234            0 => {}
235            1 => message.push_str(&format!("Did you mean `{}`?", suggestions[0])),
236            _ => {
237                message.push_str("Did you mean one of ");
238                for (idx, candidate) in suggestions.iter().enumerate() {
239                    if idx > 0 {
240                        message.push_str(", ");
241                    }
242                    message.push('`');
243                    message.push_str(candidate);
244                    message.push('`');
245                }
246                message.push('?');
247            }
248        }
249        if !fields.is_empty() {
250            let limit = 5;
251            if fields.len() > limit {
252                message.push_str(
253                    " There are too many alternatives to list here; consult the documentation!",
254                );
255            } else {
256                if suggestions.is_empty() {
257                    message.push_str("Possible alternatives are ");
258                } else if suggestions.len() == 1 {
259                    message.push_str(" The other option is ");
260                } else {
261                    message.push_str(" Other alternatives are ");
262                }
263                for (idx, candidate) in fields.iter().enumerate() {
264                    if idx > 0 {
265                        message.push_str(", ");
266                    }
267                    message.push('`');
268                    message.push_str(candidate);
269                    message.push('`');
270                }
271            }
272        }
273
274        message
275    }
276
277    pub fn field_context(
278        self,
279        type_name: &'static str,
280        field_name: &'static str,
281        obj: &Object,
282    ) -> Self {
283        let is_leaf = !matches!(self, Self::ErrorInField { .. });
284        fn add_obj_context(is_leaf: bool, obj: &Object, message: String) -> String {
285            if is_leaf {
286                // Show the object as context.
287                // However, some objects, like the main config, are very large and
288                // it isn't helpful to show that, so only include it when the context
289                // is more reasonable.
290                let obj_str = format!("{:#?}", obj);
291                if obj_str.len() > 128 || obj_str.lines().count() > 10 {
292                    message
293                } else {
294                    format!("{}.\n{}", message, obj_str)
295                }
296            } else {
297                message
298            }
299        }
300
301        match self {
302            Self::NoConversion { source_type, .. } if source_type == "Null" => Self::ErrorInField {
303                type_name,
304                field_name,
305                error: add_obj_context(is_leaf, obj, format!("missing field `{}`", field_name)),
306            },
307            Self::ErrorInField {
308                type_name: child_type,
309                field_name: child_field,
310                error,
311            } => Self::ErrorInNestedField {
312                type_name: vec![type_name, child_type],
313                field_name: vec![field_name, child_field],
314                error,
315            },
316            Self::ErrorInNestedField {
317                type_name: mut child_type,
318                field_name: mut child_field,
319                error,
320            } => Self::ErrorInNestedField {
321                type_name: {
322                    child_type.insert(0, type_name);
323                    child_type
324                },
325                field_name: {
326                    child_field.insert(0, field_name);
327                    child_field
328                },
329                error,
330            },
331            _ => Self::ErrorInField {
332                type_name,
333                field_name,
334                error: add_obj_context(is_leaf, obj, format!("{:#}", self)),
335            },
336        }
337    }
338}
339
340impl From<String> for Error {
341    fn from(s: String) -> Error {
342        Error::Message(s)
343    }
344}