Skip to main content

isoloom_compose/
error.rs

1//! Why a Compose file isn't rendered (or read back), and how a backend reports it.
2
3use isoloom_manifest::Error as ManifestError;
4use isoloom_plan::Refusal;
5use isoloom_render::Error as RenderError;
6use isoloom_spec::model::Target;
7
8/// Why a Compose file isn't rendered, or read back.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// The environment needs what the target can't do (the plan's refusal).
13    #[error("{}: {refusal}", target.id())]
14    Refused {
15        /// The target that refuses.
16        target: Target,
17        /// The first requirement it can't meet.
18        refusal: Refusal,
19    },
20    /// The Compose file couldn't be written as YAML.
21    #[error("{}: the Compose file couldn't be written as YAML", target.id())]
22    Unwritable {
23        /// The target whose file it is.
24        target: Target,
25        /// What the YAML writer reported.
26        #[source]
27        source: serde_yaml_ng::Error,
28    },
29    /// A value of the run manifest was refused by the manifest's own types: a bug of this
30    /// backend, which builds every one of them.
31    #[error("{}: the run manifest refused a value of this backend", Target::Docker.id())]
32    Manifest {
33        /// What the manifest's types refused.
34        #[source]
35        source: ManifestError,
36    },
37    /// A Compose file's text isn't YAML (reading its start plan back).
38    #[error("the Compose file isn't YAML")]
39    Unreadable {
40        /// What the YAML reader reported.
41        #[source]
42        source: serde_yaml_ng::Error,
43    },
44}
45
46/// A result whose error is this crate's [`Error`].
47pub type Result<T, E = Error> = std::result::Result<T, E>;
48
49impl From<ManifestError> for Error {
50    fn from(source: ManifestError) -> Error {
51        Error::Manifest { source }
52    }
53}
54
55impl From<Error> for RenderError {
56    /// The error as every backend reports it: the plan's refusal, or what the target doesn't
57    /// support, its cause in its words.
58    fn from(e: Error) -> RenderError {
59        match e {
60            Error::Refused { target, refusal } => RenderError::Refused { target, refusal },
61            Error::Unwritable { target, source } => unsupported(target, source.to_string()),
62            Error::Manifest { source } => unsupported(Target::Docker, format!("the run manifest: {source}")),
63            Error::Unreadable { source } => unsupported(Target::Docker, format!("the Compose file isn't YAML: {source}")),
64        }
65    }
66}
67
68fn unsupported(target: Target, what: String) -> RenderError {
69    RenderError::Unsupported { target, what }
70}
71
72#[cfg(test)]
73mod tests {
74    use std::error::Error as _;
75
76    use isoloom_plan::Requirement;
77
78    use super::*;
79
80    #[test]
81    fn a_refusal_reads_the_same_as_the_backend_reports_it() {
82        let refusal = Refusal {
83            requirement: Requirement::EnvironmentProvisioning,
84        };
85        let e = Error::Refused {
86            target: Target::Hosted,
87            refusal: refusal.clone(),
88        };
89        assert_eq!(e.to_string(), format!("hosted: {refusal}"));
90        assert_eq!(RenderError::from(e).to_string(), format!("hosted: {refusal}"));
91    }
92
93    #[test]
94    fn a_manifest_bug_keeps_its_cause_and_names_the_run_manifest() {
95        let source = isoloom_manifest::LabName::parse("Not A Name").expect_err("refused");
96        let e = Error::from(source);
97        assert!(e.source().is_some());
98        let reported = RenderError::from(e).to_string();
99        assert!(reported.starts_with("docker: the run manifest: "), "{reported}");
100    }
101}