Skip to main content

cookcli_core/
outcome.rs

1//! The result wrapper every command returns.
2
3use crate::diagnostic::Diagnostic;
4
5/// A command result paired with any non-fatal diagnostics produced along the
6/// way.
7///
8/// `cooklang` parses leniently: a recipe can parse successfully and still carry
9/// warnings. Consumers that do not care about diagnostics ignore the field.
10///
11/// # When a command returns `Err` and when it returns error diagnostics
12///
13/// An `Outcome` on the success path may still carry [`Severity::Error`]
14/// diagnostics. The rule commands follow is:
15///
16/// - Return `Err(CoreError)` when the command could not produce its value.
17///   `recipe::read` cannot return a recipe that failed to parse, so it returns
18///   `Err`.
19/// - Return `Ok(Outcome)` with error-severity diagnostics when producing the
20///   value *is* the job and the errors are the payload. `doctor::validate`
21///   reports broken recipes as data and must still succeed.
22///
23/// Callers that treat any error diagnostic as failure — a CI exit code, say —
24/// check [`has_errors`](Outcome::has_errors) in addition to the `Result`.
25///
26/// [`Severity::Error`]: crate::Severity::Error
27#[derive(Debug, Clone)]
28pub struct Outcome<T> {
29    /// What the command produced.
30    pub value: T,
31    /// Problems found while producing it. May be empty.
32    pub diagnostics: Vec<Diagnostic>,
33}
34
35impl<T> Outcome<T> {
36    /// Wrap a value with no diagnostics.
37    pub fn new(value: T) -> Self {
38        Self {
39            value,
40            diagnostics: Vec::new(),
41        }
42    }
43
44    /// Wrap a value together with diagnostics.
45    pub fn with_diagnostics(value: T, diagnostics: Vec<Diagnostic>) -> Self {
46        Self { value, diagnostics }
47    }
48
49    /// Discard diagnostics and take the value.
50    pub fn into_value(self) -> T {
51        self.value
52    }
53
54    /// True when any diagnostic has [`Severity::Error`](crate::Severity::Error).
55    pub fn has_errors(&self) -> bool {
56        self.diagnostics
57            .iter()
58            .any(|d| d.severity == crate::diagnostic::Severity::Error)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::diagnostic::Diagnostic;
66
67    #[test]
68    fn new_has_no_diagnostics() {
69        let outcome = Outcome::new(42);
70        assert_eq!(outcome.value, 42);
71        assert!(outcome.diagnostics.is_empty());
72        assert!(!outcome.has_errors());
73    }
74
75    #[test]
76    fn has_errors_detects_error_severity() {
77        let outcome = Outcome::with_diagnostics((), vec![Diagnostic::warning("just a warning")]);
78        assert!(!outcome.has_errors());
79
80        let outcome = Outcome::with_diagnostics((), vec![Diagnostic::error("a real error")]);
81        assert!(outcome.has_errors());
82
83        // A hint is not an error either.
84        let outcome = Outcome::with_diagnostics((), vec![Diagnostic::hint("a hint")]);
85        assert!(!outcome.has_errors());
86    }
87
88    #[test]
89    fn into_value_discards_diagnostics() {
90        let outcome = Outcome::with_diagnostics("hello", vec![Diagnostic::hint("hint")]);
91        assert_eq!(outcome.into_value(), "hello");
92    }
93}