use std::collections::BTreeMap;
use std::net::IpAddr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub const ENVELOPE_SCHEMA_VERSION: u8 = 1;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WebhookEnvelope {
pub schema: u8,
pub source_id: String,
pub event_kind: String,
pub body_json: serde_json::Value,
pub headers_subset: BTreeMap<String, String>,
pub received_at_ms: i64,
pub envelope_id: Uuid,
pub client_ip: Option<IpAddr>,
}
pub fn format_webhook_source(source_id: &str) -> String {
format!("webhook:{source_id}")
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> WebhookEnvelope {
WebhookEnvelope {
schema: ENVELOPE_SCHEMA_VERSION,
source_id: "github_main".into(),
event_kind: "pull_request".into(),
body_json: serde_json::json!({"action": "opened"}),
headers_subset: BTreeMap::new(),
received_at_ms: 1_700_000_000,
envelope_id: Uuid::nil(),
client_ip: None,
}
}
#[test]
fn schema_constant_locked_at_1() {
assert_eq!(ENVELOPE_SCHEMA_VERSION, 1);
assert_eq!(sample().schema, 1);
}
#[test]
fn round_trip_through_serde() {
let original = sample();
let json = serde_json::to_string(&original).unwrap();
let back: WebhookEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(original, back);
}
#[test]
fn wire_shape_lock_down() {
let env = sample();
let v = serde_json::to_value(&env).unwrap();
for key in [
"schema",
"source_id",
"event_kind",
"body_json",
"headers_subset",
"received_at_ms",
"envelope_id",
"client_ip",
] {
assert!(v.get(key).is_some(), "missing key `{key}` in envelope");
}
}
#[test]
fn format_webhook_source_prefixes() {
assert_eq!(format_webhook_source("github_main"), "webhook:github_main");
assert_eq!(format_webhook_source(""), "webhook:");
}
}