Skip to main content

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}