use serde::Serialize;
use strum::{Display as StrumDisplay, EnumString};
#[derive(
Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, EnumString, StrumDisplay, Serialize,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum SpecCommandEffect {
Read,
Write,
Destructive,
}
impl SpecCommandEffect {
pub fn as_str(&self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
Self::Destructive => "destructive",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Read => "read-only",
Self::Write => "modifies state",
Self::Destructive => "destructive",
}
}
}
pub(crate) const EFFECT_VALUES: &str = "read, write, destructive";
impl crate::SpecCommand {
pub fn effect_of<'a>(
&self,
flags: impl IntoIterator<Item = &'a crate::SpecFlag>,
args: impl IntoIterator<Item = &'a crate::SpecArg>,
) -> Option<SpecCommandEffect> {
flags
.into_iter()
.flat_map(|f| [f.effect, f.arg.as_ref().and_then(|a| a.effect)])
.flatten()
.chain(args.into_iter().filter_map(|a| a.effect))
.chain(self.effect)
.max()
}
pub fn max_effect(&self) -> Option<SpecCommandEffect> {
self.effect_of(&self.flags, &self.args)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_parse() {
assert_eq!(
SpecCommandEffect::from_str("read").unwrap(),
SpecCommandEffect::Read
);
assert_eq!(
SpecCommandEffect::from_str("destructive").unwrap(),
SpecCommandEffect::Destructive
);
assert!(SpecCommandEffect::from_str("readonly").is_err());
}
#[test]
fn test_ordering_is_least_to_most_dangerous() {
assert!(SpecCommandEffect::Read < SpecCommandEffect::Write);
assert!(SpecCommandEffect::Write < SpecCommandEffect::Destructive);
}
#[test]
fn test_effect_of_takes_the_maximum() {
use crate::Spec;
let spec: Spec = r#"
bin "pitchfork"
cmd "logs" effect="read" {
flag "--clear" effect="destructive"
flag "--follow"
arg "[daemon]"
}
cmd "quiet" {
flag "--verbose"
}
"#
.parse()
.unwrap();
let logs = &spec.cmd.subcommands["logs"];
let clear = logs.flags.iter().find(|f| f.name == "clear").unwrap();
let follow = logs.flags.iter().find(|f| f.name == "follow").unwrap();
assert_eq!(
logs.effect_of(vec![], vec![]),
Some(SpecCommandEffect::Read)
);
assert_eq!(
logs.effect_of(vec![follow], vec![]),
Some(SpecCommandEffect::Read)
);
assert_eq!(
logs.effect_of(vec![clear], vec![]),
Some(SpecCommandEffect::Destructive)
);
assert_eq!(logs.max_effect(), Some(SpecCommandEffect::Destructive));
assert_eq!(spec.cmd.subcommands["quiet"].max_effect(), None);
}
#[test]
fn test_effect_on_a_flag_value_arg_counts() {
use crate::Spec;
let spec: Spec = r#"
bin "x"
cmd "write-to" effect="read" {
flag "--output <file>" {
arg "<file>" effect="destructive"
}
}
"#
.parse()
.unwrap();
let cmd = &spec.cmd.subcommands["write-to"];
let output = cmd.flags.iter().find(|f| f.name == "output").unwrap();
assert_eq!(
cmd.effect_of(vec![output], vec![]),
Some(SpecCommandEffect::Destructive)
);
assert_eq!(cmd.max_effect(), Some(SpecCommandEffect::Destructive));
}
#[test]
fn test_effect_on_an_arg_raises_too() {
use crate::Spec;
let spec: Spec = r#"
bin "mise"
cmd "settings" effect="read" {
arg "[setting]"
arg "[value]" effect="write"
}
"#
.parse()
.unwrap();
let cmd = &spec.cmd.subcommands["settings"];
let value = cmd.args.iter().find(|a| a.name == "value").unwrap();
assert_eq!(cmd.effect_of(vec![], vec![]), Some(SpecCommandEffect::Read));
assert_eq!(
cmd.effect_of(vec![], vec![value]),
Some(SpecCommandEffect::Write)
);
}
#[test]
fn test_unknown_effect_on_a_flag_is_an_error() {
use crate::Spec;
let err = r#"
bin "x"
cmd "y" {
flag "--z" effect="readonly"
}
"#
.parse::<Spec>()
.unwrap_err();
assert!(err.to_string().contains("Invalid usage config"));
}
#[test]
fn test_display_roundtrips() {
for effect in [
SpecCommandEffect::Read,
SpecCommandEffect::Write,
SpecCommandEffect::Destructive,
] {
assert_eq!(
SpecCommandEffect::from_str(&effect.to_string()).unwrap(),
effect
);
assert_eq!(effect.to_string(), effect.as_str());
}
}
}