usage/spec/effect.rs
1use serde::Serialize;
2use strum::{Display as StrumDisplay, EnumString};
3
4/// What running a command does to the world.
5///
6/// This is a coarse, three-way classification rather than a permission model.
7/// It exists so a spec can distinguish "safe to run to find something out" from
8/// "changes state" from "may destroy something", which is the distinction
9/// consumers keep reinventing:
10///
11/// - documentation and `--help` can mark destructive commands
12/// - a shell wrapper can require confirmation
13/// - an AI coding agent can be given an allowlist of read-only commands rather
14/// than asking about every invocation
15///
16/// It is deliberately not inherited by subcommands. `git remote` and
17/// `git remote remove` do different things, and silently inheriting an effect
18/// from a parent would make the strictest reading of a spec the wrong one.
19#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumString, StrumDisplay, Serialize)]
20#[strum(serialize_all = "snake_case")]
21#[serde(rename_all = "snake_case")]
22pub enum SpecCommandEffect {
23 /// Only inspects state. Running it twice is the same as running it once,
24 /// and not running it changes nothing.
25 Read,
26 /// Creates or modifies state, but does not remove anything the user cannot
27 /// recreate by running another command.
28 Write,
29 /// May delete or irreversibly overwrite something. Deserves a confirmation
30 /// prompt.
31 Destructive,
32}
33
34impl SpecCommandEffect {
35 pub fn as_str(&self) -> &'static str {
36 match self {
37 Self::Read => "read",
38 Self::Write => "write",
39 Self::Destructive => "destructive",
40 }
41 }
42
43 /// Human-readable label used in generated documentation.
44 pub fn label(&self) -> &'static str {
45 match self {
46 Self::Read => "read-only",
47 Self::Write => "modifies state",
48 Self::Destructive => "destructive",
49 }
50 }
51}
52
53/// The set of values accepted by `effect=`, for error messages.
54pub(crate) const EFFECT_VALUES: &str = "read, write, destructive";
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use std::str::FromStr;
60
61 #[test]
62 fn test_parse() {
63 assert_eq!(
64 SpecCommandEffect::from_str("read").unwrap(),
65 SpecCommandEffect::Read
66 );
67 assert_eq!(
68 SpecCommandEffect::from_str("destructive").unwrap(),
69 SpecCommandEffect::Destructive
70 );
71 assert!(SpecCommandEffect::from_str("readonly").is_err());
72 }
73
74 #[test]
75 fn test_display_roundtrips() {
76 for effect in [
77 SpecCommandEffect::Read,
78 SpecCommandEffect::Write,
79 SpecCommandEffect::Destructive,
80 ] {
81 assert_eq!(
82 SpecCommandEffect::from_str(&effect.to_string()).unwrap(),
83 effect
84 );
85 assert_eq!(effect.to_string(), effect.as_str());
86 }
87 }
88}