ta_changeset/
review_channel.rs1use std::fmt;
12
13use crate::interaction::{
14 ChannelCapabilities, InteractionRequest, InteractionResponse, Notification,
15};
16
17#[derive(Debug, thiserror::Error)]
19pub enum ReviewChannelError {
20 #[error("I/O error: {0}")]
21 Io(#[from] std::io::Error),
22
23 #[error("channel closed")]
24 ChannelClosed,
25
26 #[error("channel timeout")]
27 Timeout,
28
29 #[error("invalid response: {0}")]
30 InvalidResponse(String),
31
32 #[error("channel error: {0}")]
33 Other(String),
34}
35
36pub trait ReviewChannel: Send + Sync {
48 fn request_interaction(
53 &self,
54 request: &InteractionRequest,
55 ) -> Result<InteractionResponse, ReviewChannelError>;
56
57 fn notify(&self, notification: &Notification) -> Result<(), ReviewChannelError>;
62
63 fn capabilities(&self) -> ChannelCapabilities;
65
66 fn channel_id(&self) -> &str;
68}
69
70#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
72pub struct ReviewChannelConfig {
73 #[serde(default = "default_channel_type")]
75 pub channel_type: String,
76
77 #[serde(default = "default_true")]
79 pub blocking_mode: bool,
80
81 #[serde(default = "default_notification_level")]
83 pub notification_level: String,
84
85 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub channel_config: Option<serde_json::Value>,
89}
90
91fn default_channel_type() -> String {
92 "terminal".to_string()
93}
94
95fn default_true() -> bool {
96 true
97}
98
99fn default_notification_level() -> String {
100 "info".to_string()
101}
102
103impl Default for ReviewChannelConfig {
104 fn default() -> Self {
105 Self {
106 channel_type: default_channel_type(),
107 blocking_mode: true,
108 notification_level: default_notification_level(),
109 channel_config: None,
110 }
111 }
112}
113
114pub fn build_channel(
119 config: &ReviewChannelConfig,
120) -> Result<Box<dyn ReviewChannel>, ReviewChannelError> {
121 use crate::terminal_channel::{AutoApproveChannel, TerminalChannel};
122
123 match config.channel_type.as_str() {
124 "terminal" => Ok(Box::new(TerminalChannel::stdio())),
125 "auto-approve" => Ok(Box::new(AutoApproveChannel::new())),
126 "webhook" => {
127 let channel_cfg = config.channel_config.as_ref().ok_or_else(|| {
128 ReviewChannelError::Other(
129 "webhook channel requires channel_config with 'endpoint' field".into(),
130 )
131 })?;
132 let endpoint = channel_cfg
133 .get("endpoint")
134 .and_then(|v| v.as_str())
135 .ok_or_else(|| {
136 ReviewChannelError::Other("webhook channel_config missing 'endpoint'".into())
137 })?;
138 Ok(Box::new(crate::webhook_channel::WebhookChannel::new(
139 endpoint,
140 )))
141 }
142 other => Err(ReviewChannelError::Other(format!(
143 "unknown channel type: '{}'. Available: terminal, auto-approve, webhook, discord",
144 other,
145 ))),
146 }
147}
148
149impl fmt::Display for ReviewChannelConfig {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 write!(
152 f,
153 "channel={}, blocking={}, notify_level={}",
154 self.channel_type, self.blocking_mode, self.notification_level
155 )
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn review_channel_config_defaults() {
165 let config = ReviewChannelConfig::default();
166 assert_eq!(config.channel_type, "terminal");
167 assert!(config.blocking_mode);
168 assert_eq!(config.notification_level, "info");
169 }
170
171 #[test]
172 fn review_channel_config_serialization() {
173 let config = ReviewChannelConfig {
174 channel_type: "slack".into(),
175 blocking_mode: false,
176 notification_level: "debug".into(),
177 channel_config: None,
178 };
179 let json = serde_json::to_string(&config).unwrap();
180 let restored: ReviewChannelConfig = serde_json::from_str(&json).unwrap();
181 assert_eq!(restored.channel_type, "slack");
182 assert!(!restored.blocking_mode);
183 assert_eq!(restored.notification_level, "debug");
184 }
185
186 #[test]
187 fn review_channel_config_display() {
188 let config = ReviewChannelConfig::default();
189 let display = format!("{}", config);
190 assert!(display.contains("terminal"));
191 assert!(display.contains("blocking=true"));
192 }
193
194 #[test]
195 fn review_channel_error_display() {
196 let err = ReviewChannelError::ChannelClosed;
197 assert_eq!(format!("{}", err), "channel closed");
198
199 let err = ReviewChannelError::InvalidResponse("bad json".into());
200 assert_eq!(format!("{}", err), "invalid response: bad json");
201 }
202}