use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case", tag = "kind", content = "contexts")]
pub enum ApproveScope {
#[default]
None,
All,
Contexts(Vec<String>),
}
impl ApproveScope {
pub fn covers(&self, context_id: &str) -> bool {
match self {
ApproveScope::None => false,
ApproveScope::All => true,
ApproveScope::Contexts(cs) => cs
.iter()
.any(|c| crate::context_path::is_ancestor_or_self(c, context_id)),
}
}
pub fn confers_nothing(&self) -> bool {
matches!(self, ApproveScope::None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_shape_is_pinned() {
let cases = [
(ApproveScope::None, r#"{"kind":"none"}"#),
(ApproveScope::All, r#"{"kind":"all"}"#),
(
ApproveScope::Contexts(vec!["a".into(), "b/c".into()]),
r#"{"kind":"contexts","contexts":["a","b/c"]}"#,
),
];
for (scope, json) in cases {
assert_eq!(serde_json::to_string(&scope).unwrap(), json);
assert_eq!(
serde_json::from_str::<ApproveScope>(json).unwrap(),
scope,
"round trip for {json}"
);
}
}
#[test]
fn absent_defaults_to_conferring_nothing() {
assert_eq!(ApproveScope::default(), ApproveScope::None);
assert!(ApproveScope::default().confers_nothing());
}
#[test]
fn covers_is_subtree_aware() {
let scope = ApproveScope::Contexts(vec!["acme".into()]);
assert!(scope.covers("acme"));
assert!(scope.covers("acme/eng"));
assert!(!scope.covers("acme-corp"), "sibling must not match");
assert!(!scope.covers("other"));
assert!(ApproveScope::All.covers("anything"));
assert!(!ApproveScope::None.covers("anything"));
}
}