git_policy_cli/unported.rs
1//! What:
2//! The work of the installed cli-git that this executable does not do yet,
3//! as one
4//! typed list,
5//! and the notice printed when a command needs any of it.
6//! Why:
7//! A command that needs unported work must stop with exit status 2 and say what is
8//! missing.
9//! Forwarding it instead would run the command without the protection the
10//! installed wrapper gives it,
11//! and nobody would see that it happened.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! // process.stderr.write(unportedNotice({ kind: 'commit-transaction' }, 'commit')); process.exitCode = 2;
16//! ```
17
18/// What:
19/// `use` brings names from sibling files into this file;
20/// `super::` means "the parent
21/// module",
22/// where every sibling file of this crate is declared.
23/// Why:
24/// A refusal can name a policy,
25/// a lifecycle trigger,
26/// or an engine "unavailable" answer.
27///
28/// In TS you'd write (pseudocode):
29/// ```ts
30/// import { triggerName } from './policy_trigger.ts';
31/// ```
32use super::policy_engine::Unavailable;
33use super::policy_registry::{PolicyId, policy_descriptor};
34use super::policy_trigger::{Trigger, trigger_name};
35/// What:
36/// `PathBuf` is an owned filesystem path of raw bytes.
37/// Sibling the reader might
38/// expect:
39/// `String`,
40/// which must be valid UTF-8.
41/// Why:
42/// A refusal about leftover state names the directory that holds it.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// // string, but byte-preserving.
47/// ```
48use std::path::PathBuf;
49
50/// What:
51/// One piece of unported work.
52/// An `enum` is a closed set of named alternatives;
53/// some
54/// carry a value.
55/// `#[derive(...)]` asks the compiler to generate copying,
56/// debug
57/// printing and `==`.
58/// Why:
59/// Every refusal of this executable is one of these,
60/// so the list of what stops a
61/// command is in one place and a test can name the exact reason.
62///
63/// In TS you'd write (pseudocode):
64/// ```ts
65/// type Unported =
66/// | { kind: 'transaction-recovery'; directory: string }
67/// | { kind: 'lifecycle'; trigger: PolicyTrigger }
68/// | { kind: 'policy-needs'; policy: PolicyId; needs: string };
69/// ```
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub enum Unported {
72 /// Recovering,
73 /// or waiting for,
74 /// the commit transactions recorded in this directory.
75 TransactionRecovery(PathBuf),
76 /// The policy lifecycle of this trigger.
77 Lifecycle(Trigger),
78 /// Something one policy needs before it can be evaluated.
79 PolicyNeeds {
80 /// The policy that could not be evaluated.
81 policy: PolicyId,
82 /// What it needs,
83 /// in words.
84 needs: &'static str,
85 },
86}
87
88/// What:
89/// Turn the engine's "unavailable" answer into the refusal it stands for.
90/// Why:
91/// The engine reports what it could not evaluate;
92/// the wrapper refuses with one
93/// vocabulary whether the gap was found by the engine or before it.
94///
95/// In TS you'd write (pseudocode):
96/// ```ts
97/// function unportedFromUnavailable(unavailable: Unavailable): Unported;
98/// ```
99pub fn unported_from_unavailable(unavailable: Unavailable) -> Unported {
100 // `match` picks one arm per variant and binds the fields each one carries.
101 match unavailable {
102 Unavailable::Lifecycle(trigger) => return Unported::Lifecycle(trigger),
103 Unavailable::Policy { policy, needs } => return Unported::PolicyNeeds { policy, needs },
104 }
105}
106
107/// What:
108/// Name the missing work as the subject of the sentence "... is not implemented".
109/// `&Unported` borrows the reason;
110/// `String` is owned UTF-8 text.
111/// Why:
112/// The person reading the notice must learn which protection is missing,
113/// in plain
114/// words,
115/// and what they can do when there is a way to avoid needing it.
116///
117/// In TS you'd write (pseudocode):
118/// ```ts
119/// function unportedSubject(what: Unported): string;
120/// ```
121fn unported_subject(what: &Unported) -> String {
122 match what {
123 // `format!` builds owned text; `.display()` shows a path, replacing bytes that are not UTF-8.
124 Unported::TransactionRecovery(directory) => {
125 return format!(
126 "recovering or waiting for the commit transactions recorded in {}",
127 directory.display()
128 );
129 }
130 // `*trigger` copies the small trigger value out of the borrowed reason.
131 Unported::Lifecycle(trigger) => {
132 return format!("the {} policy lifecycle", trigger_name(*trigger));
133 }
134 Unported::PolicyNeeds { policy, needs } => {
135 return format!(
136 "{needs}, which the policy {} needs,",
137 policy_descriptor(*policy).name
138 );
139 }
140 }
141}
142
143/// What:
144/// The complete,
145/// line-terminated notice for a command stopped by unported work.
146/// `&str` borrows the command words as the caller typed them.
147/// Why:
148/// One sentence shape for every refusal:
149/// what is missing,
150/// that the command did not
151/// run,
152/// and where the complete implementation is.
153///
154/// In TS you'd write (pseudocode):
155/// ```ts
156/// function unportedNotice(what: Unported, command: string): string;
157/// ```
158pub fn unported_notice(what: &Unported, command: &str) -> String {
159 return format!(
160 "cli-git: {} is not implemented in this native development executable, so git \
161 {command} was not run. Run it with the installed cli-git.\n",
162 unported_subject(what)
163 );
164}
165
166/// The wording of every refusal stays out of the release executable.
167#[cfg(test)]
168#[path = "unported_tests.rs"]
169mod tests;