openmoat_core/policy/
old_names.rs1use std::collections::BTreeMap;
11
12use serde::Deserialize;
13use serde::de::IgnoredAny;
14
15use super::PolicyError;
16use crate::verdict::OLD_REVIEW;
17
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct OldNames {
22 pub ask_list: bool,
24 pub ask_default: bool,
26}
27
28impl OldNames {
29 #[must_use]
31 pub fn any(self) -> bool {
32 self.ask_list || self.ask_default
33 }
34}
35
36#[derive(Deserialize)]
39struct Probe {
40 #[serde(default)]
41 ask: Option<IgnoredAny>,
42 #[serde(default)]
43 review: Option<IgnoredAny>,
44 #[serde(default)]
45 defaults: Option<ProbeDefaults>,
46}
47
48#[derive(Deserialize)]
49#[serde(untagged)]
50enum ProbeDefaults {
51 All(String),
52 PerKind(BTreeMap<String, String>),
53 Other(IgnoredAny),
55}
56
57pub(super) fn find(yaml: &str) -> Result<OldNames, PolicyError> {
63 if !yaml.contains(OLD_REVIEW) {
64 return Ok(OldNames::default());
65 }
66 let Ok(probe) = crate::yaml::from_str::<Probe>(yaml) else {
67 return Ok(OldNames::default());
68 };
69 if probe.ask.is_some() && probe.review.is_some() {
70 return Err(PolicyError::OldAndNewName {
71 old: OLD_REVIEW,
72 new: "review",
73 });
74 }
75 let ask_default = match probe.defaults {
76 Some(ProbeDefaults::All(v)) => v == OLD_REVIEW,
77 Some(ProbeDefaults::PerKind(map)) => map.values().any(|v| v == OLD_REVIEW),
78 Some(ProbeDefaults::Other(_)) | None => false,
79 };
80 Ok(OldNames {
81 ask_list: probe.ask.is_some(),
82 ask_default,
83 })
84}
85
86#[cfg(test)]
87mod tests {
88 use super::OldNames;
89 use crate::lint::warnings;
90 use crate::{Defaults, Policy, PolicyError, RepoPolicy, Verdict};
91
92 const OLD: &str = "version: 1\ndefaults: { '*': ask, net: deny }\n\
93 ask:\n - id: publish\n shell: ['cargo publish']\n";
94
95 #[test]
96 fn a_policy_with_the_old_names_loads_as_review_and_lint_says_so() {
97 let p = Policy::parse(OLD).unwrap();
98 assert_eq!(p.review.len(), 1);
99 assert_eq!(p.review[0].id, "publish");
100 let Defaults::PerKind(map) = &p.defaults else {
101 panic!("{:?}", p.defaults)
102 };
103 assert_eq!(map["*"], Verdict::Review);
104 assert_eq!(
105 p.old_names,
106 OldNames {
107 ask_list: true,
108 ask_default: true
109 }
110 );
111 let w = warnings(&p);
112 let rules: Vec<&str> = w.iter().map(|w| w.rule.as_str()).collect();
113 assert_eq!(rules, ["ask", "defaults"], "{w:?}");
114 assert!(
115 w.iter()
116 .all(|w| w.message.contains("openmoat policy upgrade")),
117 "{w:?}"
118 );
119 }
120
121 #[test]
122 fn the_new_names_and_a_word_ask_elsewhere_warn_of_nothing() {
123 let new = OLD.replace("ask", "review");
124 let p = Policy::parse(&new).unwrap();
125 assert!(!p.old_names.any());
126 assert!(warnings(&p).is_empty(), "{:?}", warnings(&p));
127 let other = "version: 1\n# ask me\ndefaults: deny\nreview:\n - id: ask\n \
129 reason: ask first\n shell: ['ask *']\n";
130 assert!(!Policy::parse(other).unwrap().old_names.any());
131 let one = Policy::parse("version: 1\ndefaults: ask\n").unwrap();
132 assert_eq!(one.defaults, Defaults::All(Verdict::Review));
133 assert!(one.old_names.ask_default && !one.old_names.ask_list);
134 }
135
136 #[test]
137 fn a_policy_with_both_ask_and_review_is_refused() {
138 let both = format!("{OLD}review:\n - id: push\n shell: ['git push']\n");
139 let err = Policy::parse(&both).unwrap_err();
140 assert!(matches!(err, PolicyError::OldAndNewName { .. }), "{err:?}");
141 let message = err.to_string();
142 assert!(message.contains("both `ask:` and `review:`"), "{message}");
143 assert!(Policy::parse("version: 1\nask: []\nreview: []\n").is_err());
145 assert!(RepoPolicy::parse(&both).is_err());
146 }
147
148 #[test]
149 fn a_repository_policy_reads_the_old_list_name() {
150 let repo =
151 RepoPolicy::parse("version: 1\nask:\n - id: rel\n shell: ['make release']\n")
152 .unwrap();
153 assert_eq!(repo.review[0].id, "rel");
154 }
155}