Skip to main content

cordis_include/
error.rs

1//! Error type for the include layer.
2
3use std::error::Error as StdError;
4use std::fmt;
5use std::io;
6use std::path::PathBuf;
7
8/// Result alias used throughout `cordis-include`.
9pub type Result<T> = std::result::Result<T, IncludeError>;
10
11/// Errors produced while reading entry trees and loader files.
12#[derive(Debug)]
13pub enum IncludeError {
14    /// An underlying filesystem operation failed.
15    Io(io::Error),
16    /// A config file could not be parsed or serialized.
17    Parse {
18        /// Which format was being processed (`"yaml"` or `"json"`).
19        format: &'static str,
20        /// The parser or serializer error.
21        source: Box<dyn StdError + Send + Sync>,
22    },
23    /// The file extension does not map to a supported format.
24    UnknownFormat {
25        /// The path that was opened.
26        path: PathBuf,
27    },
28    /// The target file is read-only, so configuration cannot be written back.
29    ReadOnly {
30        /// The path that was opened.
31        path: PathBuf,
32    },
33    /// Two entries in one tree declare the same id.
34    DuplicateId {
35        /// The conflicting id.
36        id: String,
37    },
38    /// No entry with the requested id exists in the tree.
39    EntryNotFound {
40        /// The requested (possibly composite) id.
41        id: String,
42    },
43    /// An entry id is empty or contains the `:` path separator.
44    InvalidId {
45        /// The offending id.
46        id: String,
47    },
48    /// An entry has no plugin name.
49    InvalidName,
50    /// A `${{ env.NAME }}` template referenced an unset variable.
51    MissingEnv {
52        /// The full expression that failed, e.g. `env.MISSING`.
53        expression: String,
54    },
55    /// A `${{ ... }}` template used an unsupported expression.
56    UnknownExpression {
57        /// The unsupported expression.
58        expression: String,
59    },
60    /// A `!!js` expression failed to parse or falls outside the supported
61    /// subset ([`crate::expr`]).
62    JsExpression {
63        /// The raw expression text.
64        expression: String,
65        /// What went wrong: a syntax problem, an operator or reference
66        /// outside the subset, or an unusable result.
67        message: String,
68    },
69    /// A `${{` template was never closed with `}}`.
70    Unterminated {
71        /// The input containing the dangling `${{`.
72        input: String,
73    },
74    /// The referenced entry does not belong to this tree.
75    NotInTree,
76    /// The requested move would place an entry inside its own subtree.
77    Cycle,
78    /// Setting up or running a file watcher failed (`watch` feature).
79    Watch {
80        /// The underlying watcher error.
81        source: Box<dyn StdError + Send + Sync>,
82    },
83    /// Free-form message from layers composing multiple files (import
84    /// cycles, unreadable sub-files).
85    Message {
86        /// The message.
87        message: String,
88    },
89}
90
91impl fmt::Display for IncludeError {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            Self::Io(error) => write!(f, "filesystem error: {error}"),
95            Self::Parse { format, source } => write!(f, "{format} parse error: {source}"),
96            Self::UnknownFormat { path } => write!(
97                f,
98                "unsupported config format (expected .yml, .yaml, or .json): {}",
99                path.display()
100            ),
101            Self::ReadOnly { path } => {
102                write!(f, "config file is read-only: {}", path.display())
103            }
104            Self::DuplicateId { id } => write!(f, "duplicate entry id `{id}`"),
105            Self::EntryNotFound { id } => write!(f, "entry `{id}` not found"),
106            Self::InvalidId { id } => {
107                write!(
108                    f,
109                    "invalid entry id `{id}`: must be non-empty and contain no `:`"
110                )
111            }
112            Self::InvalidName => write!(f, "entry name must be non-empty"),
113            Self::MissingEnv { expression } => {
114                write!(f, "environment variable unset: `{expression}`")
115            }
116            Self::UnknownExpression { expression } => {
117                write!(f, "unsupported template expression `{expression}`")
118            }
119            Self::JsExpression {
120                expression,
121                message,
122            } => write!(f, "!!js expression `{expression}` failed: {message}"),
123            Self::Unterminated { input } => {
124                write!(f, "unterminated template `${{{{`}} in `{input}`")
125            }
126            Self::NotInTree => write!(f, "entry does not belong to this tree"),
127            Self::Cycle => write!(f, "cannot move an entry into its own subtree"),
128            Self::Watch { source } => write!(f, "file watcher error: {source}"),
129            Self::Message { message } => write!(f, "{message}"),
130        }
131    }
132}
133
134impl StdError for IncludeError {
135    fn source(&self) -> Option<&(dyn StdError + 'static)> {
136        match self {
137            Self::Io(error) => Some(error),
138            Self::Parse { source, .. } => Some(source.as_ref()),
139            Self::Watch { source } => Some(source.as_ref()),
140            _ => None,
141        }
142    }
143}
144
145impl From<io::Error> for IncludeError {
146    fn from(error: io::Error) -> Self {
147        Self::Io(error)
148    }
149}