use serde::{Deserialize, Serialize};
use crate::error::ConfigError;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProvisionTier {
DryRun,
Provision,
VerifyOnly,
#[default]
Refuse,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ManagedResource {
pub kind: String,
pub id: String,
}
impl ManagedResource {
pub fn new(kind: impl Into<String>, id: impl Into<String>) -> Self {
Self {
kind: kind.into(),
id: id.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ManagedNotification {
pub version: u32,
pub function: String,
pub prefix: String,
pub provider: String,
pub resources: Vec<ManagedResource>,
pub updated_at_unix: u64,
}
impl Default for ManagedNotification {
fn default() -> Self {
Self {
version: crate::SCHEMA_VERSION,
function: String::new(),
prefix: String::new(),
provider: String::new(),
resources: Vec::new(),
updated_at_unix: 0,
}
}
}
impl ManagedNotification {
pub fn new(
function: &str,
prefix: &str,
provider: &str,
resources: Vec<ManagedResource>,
now_unix: u64,
) -> Self {
Self {
version: crate::SCHEMA_VERSION,
function: function.to_string(),
prefix: prefix.to_string(),
provider: provider.to_string(),
resources,
updated_at_unix: now_unix,
}
}
pub fn to_json(&self) -> Result<Vec<u8>, ConfigError> {
serde_json::to_vec(self).map_err(|err| ConfigError::parse(err.to_string()))
}
pub fn from_json(bytes: &[u8]) -> Result<Self, ConfigError> {
serde_json::from_slice(bytes).map_err(|err| ConfigError::parse(err.to_string()))
}
}
pub fn prefix_slug(prefix: &str) -> String {
let mut out = String::new();
let mut prev_dash = false;
for ch in prefix.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
prev_dash = false;
} else if !prev_dash && !out.is_empty() {
out.push('-');
prev_dash = true;
}
}
let trimmed = out.trim_matches('-');
if trimmed.is_empty() {
"root".to_string()
} else {
trimmed.to_string()
}
}
pub fn blobnotify_key(project: &str, function: &str, prefix: &str) -> String {
format!(
"project/{project}/blobnotify/{function}/{}",
prefix_slug(prefix)
)
}
pub fn blobnotify_function_prefix(project: &str, function: &str) -> String {
format!("project/{project}/blobnotify/{function}/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slug_and_keyspace_are_stable() {
assert_eq!(prefix_slug("uploads/2024/"), "uploads-2024");
assert_eq!(prefix_slug("/"), "root");
assert_eq!(prefix_slug(""), "root");
assert_eq!(
blobnotify_key("default", "ingest", "uploads/"),
"project/default/blobnotify/ingest/uploads"
);
assert_eq!(
blobnotify_function_prefix("default", "ingest"),
"project/default/blobnotify/ingest/"
);
}
#[test]
fn ledger_round_trips() {
let led = ManagedNotification::new(
"ingest",
"uploads/",
"s3",
vec![
ManagedResource::new("sqs-queue", "arn:aws:sqs:…:boatramp-ingest"),
ManagedResource::new("bucket-notification", "boatramp-ingest-uploads"),
],
42,
);
let bytes = led.to_json().unwrap();
assert_eq!(ManagedNotification::from_json(&bytes).unwrap(), led);
assert!(String::from_utf8_lossy(&bytes).contains("\"provider\":\"s3\""));
}
#[test]
fn tier_defaults_to_refuse() {
assert_eq!(ProvisionTier::default(), ProvisionTier::Refuse);
let json = serde_json::to_string(&ProvisionTier::VerifyOnly).unwrap();
assert_eq!(json, "\"verify-only\"");
}
}