1use std::fmt;
8
9use oxide_batch::AuthorizationClass;
10
11#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13#[non_exhaustive]
14pub enum Command {
15 JobList,
17 JobShow,
19 InstanceList,
21 InstanceShow,
23 ExecutionList,
25 ExecutionShow,
27 ExecutionSteps,
29 ExecutionPartitions,
31 ExecutionHistory,
33 ExecutionStop,
35 ExecutionRestart,
37 ExecutionAbandon,
39 ExecutionRecover,
41 Launch,
43 RetentionPlan,
45 RetentionApply,
47 RetentionHold,
49 RetentionRelease,
51 ConfigShow,
53 SchemaStatus,
55 DiagnosticsBundle,
57}
58
59impl Command {
60 #[must_use]
65 pub fn resolve(words: &[&str]) -> Option<Self> {
66 match words {
67 ["launch"] => Some(Self::Launch),
68 ["job", "list"] => Some(Self::JobList),
69 ["job", "show"] => Some(Self::JobShow),
70 ["instance", "list"] => Some(Self::InstanceList),
71 ["instance", "show"] => Some(Self::InstanceShow),
72 ["execution", "list"] => Some(Self::ExecutionList),
73 ["execution", "show"] => Some(Self::ExecutionShow),
74 ["execution", "steps"] => Some(Self::ExecutionSteps),
75 ["execution", "partitions"] => Some(Self::ExecutionPartitions),
76 ["execution", "history"] => Some(Self::ExecutionHistory),
77 ["execution", "stop"] => Some(Self::ExecutionStop),
78 ["execution", "restart"] => Some(Self::ExecutionRestart),
79 ["execution", "abandon"] => Some(Self::ExecutionAbandon),
80 ["execution", "recover"] => Some(Self::ExecutionRecover),
81 ["retention", "plan"] => Some(Self::RetentionPlan),
82 ["retention", "apply"] => Some(Self::RetentionApply),
83 ["retention", "hold"] => Some(Self::RetentionHold),
84 ["retention", "release"] => Some(Self::RetentionRelease),
85 ["config", "show"] => Some(Self::ConfigShow),
86 ["schema", "status"] => Some(Self::SchemaStatus),
87 ["diagnostics", "bundle"] => Some(Self::DiagnosticsBundle),
88 _ => None,
89 }
90 }
91
92 #[must_use]
94 pub const fn as_str(self) -> &'static str {
95 match self {
96 Self::JobList => "job list",
97 Self::JobShow => "job show",
98 Self::InstanceList => "instance list",
99 Self::InstanceShow => "instance show",
100 Self::ExecutionList => "execution list",
101 Self::ExecutionShow => "execution show",
102 Self::ExecutionSteps => "execution steps",
103 Self::ExecutionPartitions => "execution partitions",
104 Self::ExecutionHistory => "execution history",
105 Self::ExecutionStop => "execution stop",
106 Self::ExecutionRestart => "execution restart",
107 Self::ExecutionAbandon => "execution abandon",
108 Self::ExecutionRecover => "execution recover",
109 Self::Launch => "launch",
110 Self::RetentionPlan => "retention plan",
111 Self::RetentionApply => "retention apply",
112 Self::RetentionHold => "retention hold",
113 Self::RetentionRelease => "retention release",
114 Self::ConfigShow => "config show",
115 Self::SchemaStatus => "schema status",
116 Self::DiagnosticsBundle => "diagnostics bundle",
117 }
118 }
119
120 #[must_use]
122 pub const fn class(self) -> ActionClass {
123 match self {
124 Self::JobList
125 | Self::JobShow
126 | Self::InstanceList
127 | Self::InstanceShow
128 | Self::ExecutionList
129 | Self::ExecutionShow
130 | Self::ExecutionSteps
131 | Self::ExecutionPartitions
132 | Self::ExecutionHistory
133 | Self::RetentionPlan
134 | Self::ConfigShow
135 | Self::SchemaStatus
136 | Self::DiagnosticsBundle => ActionClass::Read,
137 Self::ExecutionStop | Self::ExecutionRestart | Self::Launch => ActionClass::Lifecycle,
138 Self::ExecutionAbandon
139 | Self::ExecutionRecover
140 | Self::RetentionApply
141 | Self::RetentionHold
142 | Self::RetentionRelease => ActionClass::Destructive,
143 }
144 }
145
146 #[must_use]
148 pub const fn is_mutating(self) -> bool {
149 !matches!(self.class(), ActionClass::Read)
150 }
151
152 #[must_use]
157 pub const fn supports_dry_run(self) -> bool {
158 matches!(
159 self,
160 Self::Launch | Self::ExecutionRestart | Self::ExecutionRecover | Self::RetentionApply
161 )
162 }
163
164 #[must_use]
166 pub const fn is_paginated(self) -> bool {
167 matches!(
168 self,
169 Self::JobList
170 | Self::InstanceList
171 | Self::ExecutionList
172 | Self::ExecutionSteps
173 | Self::ExecutionPartitions
174 | Self::ExecutionHistory
175 )
176 }
177
178 #[must_use]
180 pub const fn needs_repository(self) -> bool {
181 !matches!(self, Self::ConfigShow)
182 }
183
184 #[must_use]
186 pub const fn all() -> &'static [Self] {
187 &[
188 Self::JobList,
189 Self::JobShow,
190 Self::InstanceList,
191 Self::InstanceShow,
192 Self::ExecutionList,
193 Self::ExecutionShow,
194 Self::ExecutionSteps,
195 Self::ExecutionPartitions,
196 Self::ExecutionHistory,
197 Self::ExecutionStop,
198 Self::ExecutionRestart,
199 Self::ExecutionAbandon,
200 Self::ExecutionRecover,
201 Self::Launch,
202 Self::RetentionPlan,
203 Self::RetentionApply,
204 Self::RetentionHold,
205 Self::RetentionRelease,
206 Self::ConfigShow,
207 Self::SchemaStatus,
208 Self::DiagnosticsBundle,
209 ]
210 }
211}
212
213impl fmt::Display for Command {
214 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215 formatter.write_str(self.as_str())
216 }
217}
218
219#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
224#[non_exhaustive]
225pub enum ActionClass {
226 Read,
228 Lifecycle,
230 Destructive,
232}
233
234impl ActionClass {
235 #[must_use]
237 pub const fn as_str(self) -> &'static str {
238 match self {
239 Self::Read => "READ",
240 Self::Lifecycle => "LIFECYCLE",
241 Self::Destructive => "DESTRUCTIVE",
242 }
243 }
244
245 #[must_use]
247 pub const fn requires_confirmation(self) -> bool {
248 matches!(self, Self::Destructive)
249 }
250}
251
252impl From<AuthorizationClass> for ActionClass {
253 fn from(value: AuthorizationClass) -> Self {
254 match value {
255 AuthorizationClass::Lifecycle => Self::Lifecycle,
256 AuthorizationClass::Destructive => Self::Destructive,
257 _ => Self::Read,
258 }
259 }
260}
261
262impl fmt::Display for ActionClass {
263 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
264 formatter.write_str(self.as_str())
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 #![allow(clippy::expect_used, clippy::panic)]
271
272 use super::{ActionClass, Command};
273
274 #[test]
275 fn every_command_resolves_from_its_canonical_name() {
276 for command in Command::all() {
277 let words: Vec<&str> = command.as_str().split(' ').collect();
278 assert_eq!(Command::resolve(&words), Some(*command));
279 }
280 }
281
282 #[test]
283 fn unknown_words_do_not_resolve() {
284 assert_eq!(Command::resolve(&["job", "delete"]), None);
285 assert_eq!(Command::resolve(&["jobs", "list"]), None);
286 assert_eq!(Command::resolve(&["launch", "now"]), None);
287 assert_eq!(Command::resolve(&[]), None);
288 }
289
290 #[test]
291 fn destructive_commands_require_confirmation() {
292 for command in Command::all() {
293 assert_eq!(
294 command.class().requires_confirmation(),
295 matches!(command.class(), ActionClass::Destructive),
296 "{command} confirmation rule disagrees with its class"
297 );
298 }
299 }
300
301 #[test]
302 fn read_commands_never_mutate() {
303 for command in Command::all() {
304 if matches!(command.class(), ActionClass::Read) {
305 assert!(!command.is_mutating(), "{command} is read but mutating");
306 }
307 }
308 }
309}