use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Capability {
WireV2,
JwsAgentCard,
CardEtag,
A2ABridge,
EtereCitizenTrust,
SafeHttp,
ClockSkew60s,
StreamingTransfer,
DeferredDecision,
}
impl Capability {
pub const ALL: &'static [Capability] = &[
Capability::WireV2,
Capability::JwsAgentCard,
Capability::CardEtag,
Capability::A2ABridge,
Capability::EtereCitizenTrust,
Capability::SafeHttp,
Capability::ClockSkew60s,
Capability::StreamingTransfer,
Capability::DeferredDecision,
];
pub const fn as_bit(self) -> u8 {
match self {
Capability::WireV2 => 0,
Capability::JwsAgentCard => 1,
Capability::CardEtag => 2,
Capability::A2ABridge => 3,
Capability::EtereCitizenTrust => 4,
Capability::SafeHttp => 5,
Capability::ClockSkew60s => 6,
Capability::StreamingTransfer => 7,
Capability::DeferredDecision => 8,
}
}
pub const fn as_str(self) -> &'static str {
match self {
Capability::WireV2 => "wire-v2",
Capability::JwsAgentCard => "jws-agent-card",
Capability::CardEtag => "card-etag",
Capability::A2ABridge => "a2a-bridge",
Capability::EtereCitizenTrust => "etere-citizen-trust",
Capability::SafeHttp => "safe-http",
Capability::ClockSkew60s => "clock-skew-60s",
Capability::StreamingTransfer => "streaming-transfer",
Capability::DeferredDecision => "deferred-decision",
}
}
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|c| c.as_str() == s)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CapabilitySet(u64);
impl CapabilitySet {
pub const fn empty() -> Self {
Self(0)
}
pub fn with(mut self, cap: Capability) -> Self {
self.0 |= 1u64 << cap.as_bit();
self
}
pub fn has(self, cap: Capability) -> bool {
(self.0 & (1u64 << cap.as_bit())) != 0
}
pub fn iter(self) -> impl Iterator<Item = Capability> {
Capability::ALL
.iter()
.copied()
.filter(move |c| self.has(*c))
}
pub fn to_string_array(self) -> Vec<&'static str> {
self.iter().map(Capability::as_str).collect()
}
pub fn from_string_array<I, S>(items: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut set = Self::empty();
for item in items {
if let Some(cap) = Capability::parse(item.as_ref()) {
set = set.with(cap);
}
}
set
}
pub const fn bits(self) -> u64 {
self.0
}
}
impl Serialize for CapabilitySet {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.to_string_array().serialize(s)
}
}
impl<'de> Deserialize<'de> for CapabilitySet {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let v: Vec<String> = Vec::deserialize(d)?;
Ok(Self::from_string_array(v))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_set_has_no_caps() {
let set = CapabilitySet::empty();
for cap in Capability::ALL {
assert!(!set.has(*cap), "empty set should not have {:?}", cap);
}
}
#[test]
fn add_and_query() {
let set = CapabilitySet::empty()
.with(Capability::WireV2)
.with(Capability::JwsAgentCard);
assert!(set.has(Capability::WireV2));
assert!(set.has(Capability::JwsAgentCard));
assert!(!set.has(Capability::A2ABridge));
}
#[test]
fn bits_are_stable() {
assert_eq!(Capability::WireV2.as_bit(), 0);
assert_eq!(Capability::JwsAgentCard.as_bit(), 1);
assert_eq!(Capability::CardEtag.as_bit(), 2);
assert_eq!(Capability::A2ABridge.as_bit(), 3);
assert_eq!(Capability::EtereCitizenTrust.as_bit(), 4);
assert_eq!(Capability::SafeHttp.as_bit(), 5);
assert_eq!(Capability::ClockSkew60s.as_bit(), 6);
assert_eq!(Capability::StreamingTransfer.as_bit(), 7);
assert_eq!(Capability::DeferredDecision.as_bit(), 8);
}
#[test]
fn names_are_stable() {
assert_eq!(Capability::WireV2.as_str(), "wire-v2");
assert_eq!(Capability::JwsAgentCard.as_str(), "jws-agent-card");
assert_eq!(Capability::CardEtag.as_str(), "card-etag");
assert_eq!(Capability::A2ABridge.as_str(), "a2a-bridge");
assert_eq!(
Capability::EtereCitizenTrust.as_str(),
"etere-citizen-trust"
);
assert_eq!(Capability::SafeHttp.as_str(), "safe-http");
assert_eq!(Capability::ClockSkew60s.as_str(), "clock-skew-60s");
assert_eq!(Capability::StreamingTransfer.as_str(), "streaming-transfer");
assert_eq!(Capability::DeferredDecision.as_str(), "deferred-decision");
}
#[test]
fn parse_roundtrip() {
for cap in Capability::ALL {
let parsed = Capability::parse(cap.as_str()).unwrap();
assert_eq!(parsed, *cap);
}
assert!(Capability::parse("does-not-exist").is_none());
}
#[test]
fn iter_in_canonical_order() {
let set = CapabilitySet::empty()
.with(Capability::StreamingTransfer)
.with(Capability::WireV2)
.with(Capability::JwsAgentCard);
let order: Vec<_> = set.iter().collect();
assert_eq!(
order,
vec![
Capability::WireV2,
Capability::JwsAgentCard,
Capability::StreamingTransfer
]
);
}
#[test]
fn serde_roundtrip_via_string_array() {
let set = CapabilitySet::empty()
.with(Capability::WireV2)
.with(Capability::JwsAgentCard)
.with(Capability::CardEtag);
let json = serde_json::to_string(&set).unwrap();
assert_eq!(json, r#"["wire-v2","jws-agent-card","card-etag"]"#);
let back: CapabilitySet = serde_json::from_str(&json).unwrap();
assert_eq!(set, back);
}
#[test]
fn deserialize_skips_unknown_names() {
let json = r#"["wire-v2","post-quantum-sig","jws-agent-card"]"#;
let set: CapabilitySet = serde_json::from_str(json).unwrap();
assert!(set.has(Capability::WireV2));
assert!(set.has(Capability::JwsAgentCard));
assert_eq!(set.to_string_array().len(), 2);
}
#[test]
fn duplicate_names_idempotent() {
let set = CapabilitySet::from_string_array(["wire-v2", "wire-v2", "wire-v2"]);
assert!(set.has(Capability::WireV2));
assert_eq!(set.to_string_array().len(), 1);
}
#[test]
fn empty_array_is_empty_set() {
let set: CapabilitySet = serde_json::from_str("[]").unwrap();
assert_eq!(set, CapabilitySet::empty());
assert_eq!(set.bits(), 0);
}
#[test]
fn all_caps_set() {
let mut set = CapabilitySet::empty();
for cap in Capability::ALL {
set = set.with(*cap);
}
for cap in Capability::ALL {
assert!(set.has(*cap));
}
}
}