use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Policy {
pub id: Uuid,
pub purpose: PolicyPurpose,
pub rego_source: String,
#[serde(with = "hex32")]
pub sha256: [u8; 32],
#[serde(default)]
pub activated_at: Option<DateTime<Utc>>,
pub author_did: String,
pub created_at: DateTime<Utc>,
pub version: u32,
}
pub const POLICY_SOURCE_MAX_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub enum PolicyPurpose {
Join,
Removal,
Personhood,
Registry,
Directory,
RoleDefinitions,
CrossCommunityRoles,
CrossCommunityRelationships,
Relationships,
}
impl PolicyPurpose {
pub const ALL: [PolicyPurpose; 9] = [
PolicyPurpose::Join,
PolicyPurpose::Removal,
PolicyPurpose::Personhood,
PolicyPurpose::Registry,
PolicyPurpose::Directory,
PolicyPurpose::RoleDefinitions,
PolicyPurpose::CrossCommunityRoles,
PolicyPurpose::CrossCommunityRelationships,
PolicyPurpose::Relationships,
];
pub fn as_str(self) -> &'static str {
match self {
PolicyPurpose::Join => "join",
PolicyPurpose::Removal => "removal",
PolicyPurpose::Personhood => "personhood",
PolicyPurpose::Registry => "registry",
PolicyPurpose::Directory => "directory",
PolicyPurpose::RoleDefinitions => "roleDefinitions",
PolicyPurpose::CrossCommunityRoles => "crossCommunityRoles",
PolicyPurpose::CrossCommunityRelationships => "crossCommunityRelationships",
PolicyPurpose::Relationships => "relationships",
}
}
}
mod hex32 {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8; 32], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(&hex::encode(bytes))
}
pub fn deserialize<'de, D>(d: D) -> Result<[u8; 32], D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(d)?;
let v = hex::decode(&s).map_err(serde::de::Error::custom)?;
v.try_into()
.map_err(|got: Vec<u8>| serde::de::Error::invalid_length(got.len(), &"32 bytes"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use sha2::{Digest, Sha256};
#[test]
fn purpose_wire_shape_round_trips() {
let cases = [
(PolicyPurpose::Join, json!("join")),
(PolicyPurpose::Removal, json!("removal")),
(PolicyPurpose::Personhood, json!("personhood")),
(PolicyPurpose::Registry, json!("registry")),
(PolicyPurpose::Directory, json!("directory")),
(PolicyPurpose::RoleDefinitions, json!("roleDefinitions")),
(
PolicyPurpose::CrossCommunityRoles,
json!("crossCommunityRoles"),
),
(
PolicyPurpose::CrossCommunityRelationships,
json!("crossCommunityRelationships"),
),
(PolicyPurpose::Relationships, json!("relationships")),
];
for (purpose, wire) in cases {
assert_eq!(serde_json::to_value(purpose).unwrap(), wire);
let parsed: PolicyPurpose = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(parsed, purpose);
assert_eq!(purpose.as_str(), wire.as_str().unwrap());
}
}
#[test]
fn policy_round_trips_through_json() {
let source = "package vtc.test\nimport rego.v1\n";
let sha256: [u8; 32] = Sha256::digest(source.as_bytes()).into();
let policy = Policy {
id: Uuid::from_u128(0xdead_beef_0000_0001_0000_0000_0000_0000),
purpose: PolicyPurpose::Removal,
rego_source: source.into(),
sha256,
activated_at: None,
author_did: "did:key:zAdmin".into(),
created_at: Utc::now(),
version: 7,
};
let json = serde_json::to_value(&policy).unwrap();
assert!(json["regoSource"].is_string());
assert_eq!(json["purpose"], "removal");
assert_eq!(json["version"], 7);
let parsed: Policy = serde_json::from_value(json).unwrap();
assert_eq!(parsed, policy);
}
#[test]
fn all_covers_every_variant() {
assert_eq!(PolicyPurpose::ALL.len(), 9);
for purpose in PolicyPurpose::ALL {
let _ = purpose.as_str();
}
}
}