Skip to main content

ralph/webhook/
types.rs

1//! Webhook type definitions.
2//!
3//! Responsibilities:
4//! - Define webhook event types, payloads, and context structures.
5//! - Provide resolved configuration types.
6//!
7//! Not handled here:
8//! - Delivery logic (see `super::worker`).
9//! - Notification convenience functions (see `super::notifications`).
10
11use serde::{Deserialize, Serialize};
12use std::time::Duration;
13
14use crate::contracts::WebhookConfig;
15
16/// Types of webhook events that can be sent.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum WebhookEventType {
19    /// Task was created/added to queue.
20    TaskCreated,
21    /// Task status changed to Doing (execution started).
22    TaskStarted,
23    /// Task completed successfully (status Done).
24    TaskCompleted,
25    /// Task failed or was rejected.
26    TaskFailed,
27    /// Generic status change (used when specific type not applicable).
28    TaskStatusChanged,
29    /// Run loop started.
30    LoopStarted,
31    /// Run loop stopped (success, failure, or signal).
32    LoopStopped,
33    /// Phase started for a task.
34    PhaseStarted,
35    /// Phase completed for a task.
36    PhaseCompleted,
37    /// Queue became unblocked (runnable tasks available after being blocked).
38    QueueUnblocked,
39}
40
41impl WebhookEventType {
42    pub fn as_str(&self) -> &'static str {
43        match self {
44            WebhookEventType::TaskCreated => "task_created",
45            WebhookEventType::TaskStarted => "task_started",
46            WebhookEventType::TaskCompleted => "task_completed",
47            WebhookEventType::TaskFailed => "task_failed",
48            WebhookEventType::TaskStatusChanged => "task_status_changed",
49            WebhookEventType::LoopStarted => "loop_started",
50            WebhookEventType::LoopStopped => "loop_stopped",
51            WebhookEventType::PhaseStarted => "phase_started",
52            WebhookEventType::PhaseCompleted => "phase_completed",
53            WebhookEventType::QueueUnblocked => "queue_unblocked",
54        }
55    }
56}
57
58impl std::str::FromStr for WebhookEventType {
59    type Err = anyhow::Error;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        Ok(match s {
63            "task_created" => Self::TaskCreated,
64            "task_started" => Self::TaskStarted,
65            "task_completed" => Self::TaskCompleted,
66            "task_failed" => Self::TaskFailed,
67            "task_status_changed" => Self::TaskStatusChanged,
68            "loop_started" => Self::LoopStarted,
69            "loop_stopped" => Self::LoopStopped,
70            "phase_started" => Self::PhaseStarted,
71            "phase_completed" => Self::PhaseCompleted,
72            "queue_unblocked" => Self::QueueUnblocked,
73            other => anyhow::bail!(
74                "Unknown event type: {}. Supported: task_created, task_started, task_completed, task_failed, task_status_changed, loop_started, loop_stopped, phase_started, phase_completed, queue_unblocked",
75                other
76            ),
77        })
78    }
79}
80
81/// Optional context metadata for webhook payloads.
82/// These fields are only serialized when set (Some).
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84pub struct WebhookContext {
85    /// Runner used for this phase/execution (e.g., "claude", "codex").
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub runner: Option<String>,
88    /// Model used for this phase/execution.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub model: Option<String>,
91    /// Current phase number (1, 2, or 3).
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub phase: Option<u8>,
94    /// Total number of phases configured.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub phase_count: Option<u8>,
97    /// Duration in milliseconds (for completed operations).
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub duration_ms: Option<u64>,
100    /// Repository root path.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub repo_root: Option<String>,
103    /// Current git branch.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub branch: Option<String>,
106    /// Current git commit hash.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub commit: Option<String>,
109    /// CI gate outcome: "skipped", "passed", or "failed".
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub ci_gate: Option<String>,
112}
113
114/// Webhook event payload structure.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct WebhookPayload {
117    /// Event type identifier.
118    pub event: String,
119    /// Timestamp of the event (RFC3339).
120    pub timestamp: String,
121    /// Task ID (e.g., "RQ-0001").
122    /// Optional: may be None for loop-level events.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub task_id: Option<String>,
125    /// Task title.
126    /// Optional: may be None for loop-level events.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub task_title: Option<String>,
129    /// Previous status (if applicable).
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub previous_status: Option<String>,
132    /// Current/new status.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub current_status: Option<String>,
135    /// Additional context or notes.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub note: Option<String>,
138    /// Optional context metadata (runner, model, phase, git info, etc.)
139    #[serde(flatten)]
140    pub context: WebhookContext,
141}
142
143/// Resolved webhook configuration with defaults applied.
144#[derive(Debug, Clone)]
145pub struct ResolvedWebhookConfig {
146    pub enabled: bool,
147    pub url: Option<String>,
148    pub secret: Option<String>,
149    pub timeout: Duration,
150    pub retry_count: u32,
151    pub retry_backoff: Duration,
152}
153
154impl ResolvedWebhookConfig {
155    /// Resolve a WebhookConfig to concrete values with defaults.
156    pub fn from_config(config: &WebhookConfig) -> Self {
157        Self {
158            enabled: config.enabled.unwrap_or(false),
159            url: config.url.clone(),
160            secret: config.secret.clone(),
161            timeout: Duration::from_secs(config.timeout_secs.unwrap_or(30) as u64),
162            retry_count: config.retry_count.unwrap_or(3),
163            retry_backoff: Duration::from_millis(config.retry_backoff_ms.unwrap_or(1000) as u64),
164        }
165    }
166}
167
168/// Internal message for the webhook worker.
169#[derive(Debug, Clone)]
170pub(crate) struct WebhookMessage {
171    pub(crate) payload: WebhookPayload,
172    pub(crate) config: ResolvedWebhookConfig,
173}
174
175/// Resolve webhook config to concrete values.
176pub(crate) fn resolve_webhook_config(config: &WebhookConfig) -> ResolvedWebhookConfig {
177    ResolvedWebhookConfig::from_config(config)
178}