use std::fmt;
use crate::interaction::{
ChannelCapabilities, InteractionRequest, InteractionResponse, Notification,
};
#[derive(Debug, thiserror::Error)]
pub enum ReviewChannelError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("channel closed")]
ChannelClosed,
#[error("channel timeout")]
Timeout,
#[error("invalid response: {0}")]
InvalidResponse(String),
#[error("channel error: {0}")]
Other(String),
}
pub trait ReviewChannel: Send + Sync {
fn request_interaction(
&self,
request: &InteractionRequest,
) -> Result<InteractionResponse, ReviewChannelError>;
fn notify(&self, notification: &Notification) -> Result<(), ReviewChannelError>;
fn capabilities(&self) -> ChannelCapabilities;
fn channel_id(&self) -> &str;
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ReviewChannelConfig {
#[serde(default = "default_channel_type")]
pub channel_type: String,
#[serde(default = "default_true")]
pub blocking_mode: bool,
#[serde(default = "default_notification_level")]
pub notification_level: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_config: Option<serde_json::Value>,
}
fn default_channel_type() -> String {
"terminal".to_string()
}
fn default_true() -> bool {
true
}
fn default_notification_level() -> String {
"info".to_string()
}
impl Default for ReviewChannelConfig {
fn default() -> Self {
Self {
channel_type: default_channel_type(),
blocking_mode: true,
notification_level: default_notification_level(),
channel_config: None,
}
}
}
pub fn build_channel(
config: &ReviewChannelConfig,
) -> Result<Box<dyn ReviewChannel>, ReviewChannelError> {
use crate::terminal_channel::{AutoApproveChannel, TerminalChannel};
match config.channel_type.as_str() {
"terminal" => Ok(Box::new(TerminalChannel::stdio())),
"auto-approve" => Ok(Box::new(AutoApproveChannel::new())),
"webhook" => {
let channel_cfg = config.channel_config.as_ref().ok_or_else(|| {
ReviewChannelError::Other(
"webhook channel requires channel_config with 'endpoint' field".into(),
)
})?;
let endpoint = channel_cfg
.get("endpoint")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ReviewChannelError::Other("webhook channel_config missing 'endpoint'".into())
})?;
Ok(Box::new(crate::webhook_channel::WebhookChannel::new(
endpoint,
)))
}
other => Err(ReviewChannelError::Other(format!(
"unknown channel type: '{}'. Available: terminal, auto-approve, webhook, discord",
other,
))),
}
}
impl fmt::Display for ReviewChannelConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"channel={}, blocking={}, notify_level={}",
self.channel_type, self.blocking_mode, self.notification_level
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn review_channel_config_defaults() {
let config = ReviewChannelConfig::default();
assert_eq!(config.channel_type, "terminal");
assert!(config.blocking_mode);
assert_eq!(config.notification_level, "info");
}
#[test]
fn review_channel_config_serialization() {
let config = ReviewChannelConfig {
channel_type: "slack".into(),
blocking_mode: false,
notification_level: "debug".into(),
channel_config: None,
};
let json = serde_json::to_string(&config).unwrap();
let restored: ReviewChannelConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.channel_type, "slack");
assert!(!restored.blocking_mode);
assert_eq!(restored.notification_level, "debug");
}
#[test]
fn review_channel_config_display() {
let config = ReviewChannelConfig::default();
let display = format!("{}", config);
assert!(display.contains("terminal"));
assert!(display.contains("blocking=true"));
}
#[test]
fn review_channel_error_display() {
let err = ReviewChannelError::ChannelClosed;
assert_eq!(format!("{}", err), "channel closed");
let err = ReviewChannelError::InvalidResponse("bad json".into());
assert_eq!(format!("{}", err), "invalid response: bad json");
}
}