git_policy_cli/policy_trigger.rs
1//! What:
2//! The lifecycle points at which policies run,
3//! which shipped policy runs at which
4//! point,
5//! and which points this executable can serve.
6//! Why:
7//! A policy runs only for the triggers it declares.
8//! A trigger whose lifecycle is not
9//! ported must be answered as "unavailable",
10//! never as an empty result that would read
11//! as "checked and clean".
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! // type PolicyTrigger = 'pre-forward' | 'post-commit' | 'manual-push' | 'direct-check' | 'direct-fix';
16//! ```
17
18/// What:
19/// `use` brings a name from a sibling file into this file;
20/// `super::` means "the
21/// parent module",
22/// where every sibling file of this crate is declared.
23/// Why:
24/// The trigger table is keyed by the registry's typed policy identity.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// import { PolicyId } from './policy_registry.ts';
29/// ```
30use super::policy_registry::PolicyId;
31
32/// What:
33/// One lifecycle point.
34/// An `enum` is a closed set of named alternatives.
35/// `#[derive(...)]` asks the compiler to generate copying (`Clone`,
36/// `Copy`),
37/// debug
38/// printing (`Debug`) and `==` (`Eq`,
39/// `PartialEq`).
40/// Why:
41/// The set is fixed by the event format,
42/// where each event names its trigger.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// type PolicyTrigger = 'pre-forward' | 'post-commit' | 'manual-push' | 'direct-check' | 'direct-fix';
47/// ```
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum Trigger {
50 /// Before a wrapped Git command is forwarded.
51 PreForward,
52 /// After a commit landed,
53 /// against the landed commit.
54 PostCommit,
55 /// Before a manual `git push`,
56 /// against newly published content.
57 ManualPush,
58 /// `git cli-git check`.
59 DirectCheck,
60 /// `git cli-git fix`.
61 DirectFix,
62}
63
64/// What:
65/// The wire spelling of a trigger.
66/// `&'static str` is text baked into the program
67/// for its whole run (sibling `String` would be an owned copy).
68/// Why:
69/// Events print exactly the spellings the incumbent emitted.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// function triggerName(trigger: PolicyTrigger): string { return trigger; }
74/// ```
75pub fn trigger_name(trigger: Trigger) -> &'static str {
76 // `match` picks one arm per variant; the compiler refuses the code if one is forgotten.
77 match trigger {
78 Trigger::PreForward => return "pre-forward",
79 Trigger::PostCommit => return "post-commit",
80 Trigger::ManualPush => return "manual-push",
81 Trigger::DirectCheck => return "direct-check",
82 Trigger::DirectFix => return "direct-fix",
83 }
84}
85
86/// What:
87/// Every trigger.
88/// `&[Trigger]` is a borrowed,
89/// read-only list (siblings:
90/// `Vec<T>`,
91/// owned and growable;
92/// `[T; N]`,
93/// fixed length in the type).
94/// Why:
95/// Two shipped policies check content at every lifecycle point.
96///
97/// In TS you'd write (pseudocode):
98/// ```ts
99/// const EVERY_TRIGGER = ['pre-forward', 'post-commit', 'manual-push', 'direct-check', 'direct-fix'] as const;
100/// ```
101const EVERY_TRIGGER: &[Trigger] = &[
102 Trigger::PreForward,
103 Trigger::PostCommit,
104 Trigger::ManualPush,
105 Trigger::DirectCheck,
106 Trigger::DirectFix,
107];
108
109/// What:
110/// The triggers a shipped policy declares,
111/// copied from the incumbent definitions.
112/// Why:
113/// The engine skips a policy at every other lifecycle point,
114/// exactly as the
115/// incumbent's stage did.
116///
117/// In TS you'd write (pseudocode):
118/// ```ts
119/// function policyTriggers(id: PolicyId): readonly PolicyTrigger[];
120/// ```
121pub fn policy_triggers(id: PolicyId) -> &'static [Trigger] {
122 // `|` joins patterns that share one arm; `&[...]` is a list baked into the program.
123 match id {
124 PolicyId::RequireRoot | PolicyId::ForbiddenRootContext => {
125 return &[Trigger::PreForward, Trigger::DirectCheck];
126 }
127 PolicyId::LinkedWorktreeOnly | PolicyId::BranchWorktreeOnly | PolicyId::AddExplicit => {
128 return &[Trigger::PreForward];
129 }
130 PolicyId::FinalNewline | PolicyId::MarkdownAutofix => return EVERY_TRIGGER,
131 PolicyId::DependentVersionBump => {
132 return &[
133 Trigger::PreForward,
134 Trigger::DirectCheck,
135 Trigger::DirectFix,
136 ];
137 }
138 PolicyId::ForbiddenStrings => {
139 return &[
140 Trigger::PreForward,
141 Trigger::PostCommit,
142 Trigger::ManualPush,
143 Trigger::DirectCheck,
144 ];
145 }
146 }
147}
148
149/// What:
150/// Whether a policy declares a trigger.
151/// `bool` is true or false.
152/// Why:
153/// The stage asks this once per policy before anything else.
154///
155/// In TS you'd write (pseudocode):
156/// ```ts
157/// const runs = policy.triggers.includes(trigger);
158/// ```
159pub fn policy_runs_on(id: PolicyId, trigger: Trigger) -> bool {
160 // `.contains(&trigger)` borrows the trigger for the lookup.
161 return policy_triggers(id).contains(&trigger);
162}
163
164/// What:
165/// Whether this executable implements the lifecycle a trigger belongs to.
166/// Why:
167/// Every lifecycle is ported: the exhaustive match below answers true for each variant, and a new
168/// variant fails to compile until it is answered here.
169/// The engine still checks this before running a lifecycle, so a future unported variant gets a typed
170/// "unavailable" instead of running zero policies and looking clean.
171///
172/// In TS you'd write (pseudocode):
173/// ```ts
174/// const ported = (trigger: Trigger): boolean => true;
175/// ```
176pub fn trigger_is_ported(trigger: Trigger) -> bool {
177 match trigger {
178 Trigger::PreForward
179 | Trigger::PostCommit
180 | Trigger::ManualPush
181 | Trigger::DirectCheck
182 | Trigger::DirectFix => {
183 return true;
184 }
185 }
186}
187
188/// Trigger sets,
189/// spellings and the ported set stay out of the release executable.
190#[cfg(test)]
191#[path = "policy_trigger_tests.rs"]
192mod tests;