use serde_json::Value;
use crate::client::Repository;
use crate::error::EventError;
use super::{EntityType, EventEnvelope, EventId, EventMetadata, EventPayload};
#[derive(Debug, Clone)]
pub struct ProcessorConfig {
pub enable_signature_validation: bool,
pub enable_session_correlation: bool,
pub session_id_strategy: SessionIdStrategy,
pub max_payload_size: usize,
pub trace_sampling_rate: f64,
}
impl Default for ProcessorConfig {
fn default() -> Self {
Self {
enable_signature_validation: true,
enable_session_correlation: true,
session_id_strategy: SessionIdStrategy::Entity,
max_payload_size: 1024 * 1024, trace_sampling_rate: 0.1,
}
}
}
#[derive(Debug, Clone)]
pub enum SessionIdStrategy {
None,
Entity,
Repository,
Custom(fn(&EventEnvelope) -> Option<String>),
}
pub struct EventProcessor {
config: ProcessorConfig,
}
impl EventProcessor {
pub fn new(config: ProcessorConfig) -> Self {
Self { config }
}
pub async fn process_webhook(
&self,
event_type: &str,
payload: &[u8],
delivery_id: Option<&str>,
) -> Result<EventEnvelope, EventError> {
if payload.len() > self.config.max_payload_size {
return Err(EventError::PayloadTooLarge {
size: payload.len(),
max: self.config.max_payload_size,
});
}
let json_payload: Value = serde_json::from_slice(payload)?;
let repository =
json_payload
.get("repository")
.ok_or_else(|| EventError::MissingField {
field: "repository".to_string(),
})?;
let repository: Repository = serde_json::from_value(repository.clone())?;
let (entity_type, entity_id) = self.extract_entity_info(event_type, &json_payload)?;
let event_payload = EventPayload::new(json_payload);
let event_id = if let Some(delivery_id) = delivery_id {
EventId::from_github_delivery(delivery_id)
} else {
EventId::new()
};
let metadata = EventMetadata {
delivery_id: delivery_id.map(|s| s.to_string()),
signature_valid: !self.config.enable_signature_validation, ..Default::default()
};
let mut envelope = EventEnvelope {
event_id,
event_type: event_type.to_string(),
repository,
entity_type,
entity_id: entity_id.clone(),
session_id: None,
payload: event_payload,
metadata,
trace_context: None,
};
if self.config.enable_session_correlation {
let session_id =
self.generate_session_id(&envelope.entity_type, &entity_id, &envelope.repository);
envelope.session_id = session_id;
}
Ok(envelope)
}
pub fn extract_entity_info(
&self,
event_type: &str,
payload: &Value,
) -> Result<(EntityType, Option<String>), EventError> {
let entity_type = EntityType::from_event_type(event_type);
let entity_id = match event_type {
"pull_request" | "pull_request_review" | "pull_request_review_comment" => {
payload
.get("number")
.or_else(|| payload.get("pull_request").and_then(|pr| pr.get("number")))
.and_then(|v| v.as_u64())
.map(|n| n.to_string())
}
"issues" | "issue_comment" => {
payload
.get("issue")
.and_then(|issue| issue.get("number"))
.and_then(|v| v.as_u64())
.map(|n| n.to_string())
}
"push" | "create" | "delete" => {
payload
.get("ref")
.and_then(|v| v.as_str())
.map(|r| r.to_string())
}
"check_run" => {
payload
.get("check_run")
.and_then(|cr| cr.get("id"))
.and_then(|v| v.as_u64())
.map(|n| n.to_string())
}
"check_suite" => {
payload
.get("check_suite")
.and_then(|cs| cs.get("id"))
.and_then(|v| v.as_u64())
.map(|n| n.to_string())
}
"release" => {
payload
.get("release")
.and_then(|r| r.get("id"))
.and_then(|v| v.as_u64())
.map(|n| n.to_string())
}
"deployment" | "deployment_status" => {
payload
.get("deployment")
.and_then(|d| d.get("id"))
.and_then(|v| v.as_u64())
.map(|n| n.to_string())
}
_ => {
None
}
};
Ok((entity_type, entity_id))
}
pub fn generate_session_id(
&self,
entity_type: &EntityType,
entity_id: &Option<String>,
repository: &Repository,
) -> Option<String> {
match &self.config.session_id_strategy {
SessionIdStrategy::None => None,
SessionIdStrategy::Entity => {
if let Some(id) = entity_id {
match entity_type {
EntityType::PullRequest => {
Some(format!("pr-{}-{}", repository.full_name, id))
}
EntityType::Issue => Some(format!("issue-{}-{}", repository.full_name, id)),
EntityType::Branch => {
Some(format!("branch-{}-{}", repository.full_name, id))
}
EntityType::CheckRun => {
Some(format!("check-run-{}-{}", repository.full_name, id))
}
EntityType::CheckSuite => {
Some(format!("check-suite-{}-{}", repository.full_name, id))
}
EntityType::Release => {
Some(format!("release-{}-{}", repository.full_name, id))
}
EntityType::Deployment => {
Some(format!("deployment-{}-{}", repository.full_name, id))
}
_ => None,
}
} else {
None
}
}
SessionIdStrategy::Repository => {
Some(format!("repo-{}", repository.full_name))
}
SessionIdStrategy::Custom(f) => {
let temp_envelope = EventEnvelope {
event_id: EventId::new(),
event_type: String::new(),
repository: repository.clone(),
entity_type: entity_type.clone(),
entity_id: entity_id.clone(),
session_id: None,
payload: EventPayload::new(serde_json::Value::Null),
metadata: EventMetadata::default(),
trace_context: None,
};
f(&temp_envelope)
}
}
}
}
#[cfg(test)]
#[path = "processor_tests.rs"]
mod tests;