git_policy_cli/candidate_error.rs
1//! What:
2//! The one failure type of the candidate layer,
3//! with a closed list of causes.
4//! Why:
5//! Every candidate failure stops policy evaluation (a file that could not be read
6//! was not checked),
7//! and callers branch on the cause without parsing message text.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! class CandidateError extends Error { readonly failure: CandidateFailure }
12//! ```
13
14/// What:
15/// Why candidate listing or reading failed.
16/// `#[derive(...)]` generates copying,
17/// debug printing and `==`.
18/// Why:
19/// The policy engine maps causes to engine-failure codes,
20/// and tests name the
21/// exact cause instead of matching diagnostic wording.
22///
23/// In TS you'd write (pseudocode):
24/// ```ts
25/// type CandidateFailure = 'git-not-started' | 'git-failed' | 'listing-malformed' | ...;
26/// ```
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum CandidateFailure {
29 /// The real Git program could not be started at all.
30 GitNotStarted,
31 /// A listing command ran and exited unsuccessfully.
32 GitFailed,
33 /// Listing output is not the NUL-delimited record format Git 2.56.0 prints.
34 ListingMalformed,
35 /// A listed entry has a file mode no candidate can carry.
36 UnsupportedMode,
37 /// A listed entry has a change status this layer does not interpret.
38 UnsupportedStatus,
39 /// The index holds a conflicted (unmerged) entry.
40 UnmergedPath,
41 /// The object reader ended,
42 /// or was closed after an earlier failure.
43 ReaderEnded,
44 /// An object reply does not follow the `cat-file --batch` format.
45 ReplyMalformed,
46 /// An object reply stopped before its declared size or terminator.
47 ReplyTruncated,
48 /// An object reply names a different object than the one requested.
49 ReplyMismatched,
50 /// Git reported the requested object as missing.
51 ObjectMissing,
52 /// The requested object exists with a type the caller cannot use.
53 ObjectKindUnexpected,
54 /// A candidate belongs to a version that was invalidated.
55 StaleCandidate,
56 /// The private index copy a prediction stages into could not be created or copied.
57 PrivateIndexUnavailable,
58}
59
60/// What:
61/// One candidate failure:
62/// its cause and a complete explanation.
63/// `String` is a heap-allocated,
64/// growable UTF-8 buffer this struct owns.
65/// Sibling the reader might expect:
66/// `&str`,
67/// a borrowed view that owns nothing.
68/// Why:
69/// The error outlives the formatted text built at the failure site,
70/// so a
71/// borrowed `&str` would dangle.
72/// Messages never contain file content or
73/// pathnames:
74/// a pathname can itself be a forbidden string.
75///
76/// In TS you'd write (pseudocode):
77/// ```ts
78/// class CandidateError extends Error { failure: CandidateFailure; message: string }
79/// ```
80#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct CandidateError {
82 /// Closed cause callers branch on.
83 pub failure: CandidateFailure,
84 /// Complete diagnostic naming the Git operation and what to check.
85 pub message: String,
86}
87
88/// What:
89/// `impl CandidateError { ... }` attaches functions to the struct,
90/// like class statics.
91/// Why:
92/// Failure sites keep their condition next to its user-facing explanation.
93///
94/// In TS you'd write (pseudocode):
95/// ```ts
96/// new CandidateError(failure, message);
97/// ```
98impl CandidateError {
99 /// What:
100 /// Build an error from a cause and borrowed text.
101 /// `&str` lends the caller's
102 /// bytes;
103 /// `String::from` copies them into storage the error owns.
104 /// Why:
105 /// Messages are usually `format!` temporaries that end with the caller's statement.
106 ///
107 /// In TS you'd write (pseudocode):
108 /// ```ts
109 /// constructor(failure: CandidateFailure, message: string) { super(message); }
110 /// ```
111 pub fn new(failure: CandidateFailure, message: &str) -> CandidateError {
112 // What: `return CandidateError { ... };` builds the struct and hands it back.
113 // Why: Rust never copies a borrowed string into an owning field implicitly.
114 //
115 // In TS you'd write (pseudocode):
116 // ```ts
117 // return { failure, message };
118 // ```
119 return CandidateError {
120 failure,
121 message: String::from(message),
122 };
123 }
124}
125
126/// What:
127/// `impl std::fmt::Display for CandidateError` supplies Rust's "print me" interface.
128/// `::` walks module paths,
129/// like `std.fmt.Display` would in TS.
130/// Why:
131/// The executable writes the message with ordinary formatting,
132/// not field access.
133///
134/// In TS you'd write (pseudocode):
135/// ```ts
136/// toString(): string { return this.message; }
137/// ```
138impl std::fmt::Display for CandidateError {
139 /// What:
140 /// `&self` borrows this error read-only;
141 /// `&mut` lends the formatter for writing;
142 /// `'_` is an unnamed lifetime the compiler fills in.
143 /// Why:
144 /// Formatting writes into the caller's buffer without allocating another string.
145 ///
146 /// In TS you'd write (pseudocode):
147 /// ```ts
148 /// format(out: Writer): void { out.write(this.message); }
149 /// ```
150 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 // `.as_str()` lends the owned message as a borrowed view for the writer.
152 return formatter.write_str(self.message.as_str());
153 }
154}
155
156/// What:
157/// An empty `impl` marks the type as a standard error value.
158/// Why:
159/// Generic error handling can carry it without converting it to a bare string.
160///
161/// In TS you'd write (pseudocode):
162/// ```ts
163/// // `extends Error` already provides this in TypeScript.
164/// ```
165impl std::error::Error for CandidateError {}
166
167/// Message controls stay out of the release executable.
168#[cfg(test)]
169#[path = "candidate_error_tests.rs"]
170mod tests;