use crate::domain::{
AssistantMessage, AssistantMessageContent, ContentBlock, ContentValue, HookContext, HookInput,
Message, ToolPermissionContext, UserMessage, UserMessageContent,
};
use crate::dsl::{FileNotificationFormat, NotificationChannel, NotificationSpec};
use serde_json::Value;
use std::collections::HashMap;
pub struct MessageBuilder {
content: Vec<ContentBlock>,
}
impl MessageBuilder {
pub fn new() -> Self {
Self {
content: Vec::new(),
}
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.content.push(ContentBlock::Text { text: text.into() });
self
}
pub fn thinking(mut self, thinking: impl Into<String>, signature: impl Into<String>) -> Self {
self.content.push(ContentBlock::Thinking {
thinking: thinking.into(),
signature: signature.into(),
});
self
}
pub fn tool_use(
mut self,
id: impl Into<String>,
name: impl Into<String>,
input: Value,
) -> Self {
self.content.push(ContentBlock::ToolUse {
id: id.into(),
name: name.into(),
input,
});
self
}
pub fn tool_result(
mut self,
tool_use_id: impl Into<String>,
content: Value,
is_error: Option<bool>,
) -> Self {
self.content.push(ContentBlock::ToolResult {
tool_use_id: tool_use_id.into(),
content: Some(content),
is_error,
});
self
}
pub fn user(text: impl Into<String>) -> Message {
Message::User(UserMessage {
message: UserMessageContent {
role: "user".to_string(),
content: ContentValue::Text(text.into()),
},
parent_tool_use_id: None,
})
}
pub fn build_assistant(self) -> Message {
Message::Assistant(AssistantMessage {
message: AssistantMessageContent {
model: "claude-sonnet-4-5".to_string(),
content: self.content,
},
parent_tool_use_id: None,
})
}
pub fn build(self) -> Vec<ContentBlock> {
self.content
}
}
impl Default for MessageBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct HookInputBuilder;
impl HookInputBuilder {
pub fn pre_tool_use(tool_name: impl Into<String>, tool_input: Value) -> HookInput {
HookInput::PreToolUse {
session_id: "test-session".to_string(),
transcript_path: "/tmp/transcript.json".to_string(),
cwd: "/tmp".to_string(),
permission_mode: None,
tool_name: tool_name.into(),
tool_input,
}
}
pub fn post_tool_use(
tool_name: impl Into<String>,
tool_input: Value,
tool_response: Value,
) -> HookInput {
HookInput::PostToolUse {
session_id: "test-session".to_string(),
transcript_path: "/tmp/transcript.json".to_string(),
cwd: "/tmp".to_string(),
permission_mode: None,
tool_name: tool_name.into(),
tool_input,
tool_response,
}
}
pub fn user_prompt_submit(prompt: impl Into<String>) -> HookInput {
HookInput::UserPromptSubmit {
session_id: "test-session".to_string(),
transcript_path: "/tmp/transcript.json".to_string(),
cwd: "/tmp".to_string(),
permission_mode: None,
prompt: prompt.into(),
}
}
pub fn stop() -> HookInput {
HookInput::Stop {
session_id: "test-session".to_string(),
transcript_path: "/tmp/transcript.json".to_string(),
cwd: "/tmp".to_string(),
permission_mode: None,
stop_hook_active: false,
}
}
}
pub struct PermissionContextBuilder {
suggestions: Vec<crate::domain::PermissionUpdate>,
}
impl PermissionContextBuilder {
pub fn new() -> Self {
Self {
suggestions: Vec::new(),
}
}
pub fn build(self) -> ToolPermissionContext {
ToolPermissionContext {
signal: None,
suggestions: self.suggestions,
}
}
}
impl Default for PermissionContextBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct HookContextBuilder;
impl HookContextBuilder {
pub fn build() -> HookContext {
HookContext { signal: None }
}
}
impl Default for HookContextBuilder {
fn default() -> Self {
Self
}
}
pub struct NotificationBuilder {
message: String,
channels: Vec<NotificationChannel>,
title: Option<String>,
priority: Option<crate::dsl::NotificationPriority>,
}
impl NotificationBuilder {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
channels: Vec::new(),
title: None,
priority: None,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn priority(mut self, priority: crate::dsl::NotificationPriority) -> Self {
self.priority = Some(priority);
self
}
pub fn console(mut self) -> Self {
self.channels.push(NotificationChannel::Console {
colored: true,
timestamp: true,
});
self
}
pub fn file(mut self, path: impl Into<String>) -> Self {
self.channels.push(NotificationChannel::File {
path: path.into(),
append: false,
timestamp: true,
format: FileNotificationFormat::Json,
});
self
}
pub fn ntfy(mut self, server: impl Into<String>, topic: impl Into<String>) -> Self {
self.channels.push(NotificationChannel::Ntfy {
server: server.into(),
topic: topic.into(),
title: None,
priority: None,
tags: Vec::new(),
click_url: None,
attach_url: None,
markdown: false,
auth_token: None,
});
self
}
pub fn build(self) -> NotificationSpec {
NotificationSpec::Structured {
message: self.message,
channels: self.channels,
title: self.title,
priority: self.priority,
metadata: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_message_builder() {
let content = MessageBuilder::new()
.text("Hello")
.tool_use("tool-1", "Read", json!({"file_path": "test.txt"}))
.build();
assert_eq!(content.len(), 2);
if let ContentBlock::Text { text } = &content[0] {
assert_eq!(text, "Hello");
} else {
panic!("Expected text block");
}
}
#[test]
fn test_user_message() {
let msg = MessageBuilder::user("Test prompt");
if let Message::User(user_msg) = msg {
if let ContentValue::Text(text) = &user_msg.message.content {
assert_eq!(text, "Test prompt");
} else {
panic!("Expected text content");
}
} else {
panic!("Expected user message");
}
}
#[test]
fn test_notification_builder() {
use crate::dsl::NotificationPriority;
let notification = NotificationBuilder::new("Test message")
.title("Test Title")
.priority(NotificationPriority::High)
.console()
.build();
if let NotificationSpec::Structured {
message,
channels,
title,
priority,
..
} = notification
{
assert_eq!(message, "Test message");
assert_eq!(channels.len(), 1);
assert_eq!(title, Some("Test Title".to_string()));
assert_eq!(priority, Some(NotificationPriority::High));
} else {
panic!("Expected Structured notification");
}
}
}