use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt;
use std::sync::Arc;
use super::extractors::{JsonPath, JsonPathSegment};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventAction {
Content,
RoleStart,
ToolCall,
Finish,
UsageUpdate,
Error,
MessageStart,
MessageDelta,
ContentBlockStart,
ContentBlockDelta,
ContentBlockStop,
Ping,
Start,
Stop,
Custom(String),
}
impl Default for EventAction {
fn default() -> Self {
EventAction::Content
}
}
impl fmt::Display for EventAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EventAction::Content => write!(f, "content"),
EventAction::RoleStart => write!(f, "role_start"),
EventAction::ToolCall => write!(f, "tool_call"),
EventAction::Finish => write!(f, "finish"),
EventAction::UsageUpdate => write!(f, "usage_update"),
EventAction::Error => write!(f, "error"),
EventAction::MessageStart => write!(f, "message_start"),
EventAction::MessageDelta => write!(f, "message_delta"),
EventAction::ContentBlockStart => write!(f, "content_block_start"),
EventAction::ContentBlockDelta => write!(f, "content_block_delta"),
EventAction::ContentBlockStop => write!(f, "content_block_stop"),
EventAction::Ping => write!(f, "ping"),
EventAction::Start => write!(f, "start"),
EventAction::Stop => write!(f, "stop"),
EventAction::Custom(name) => write!(f, "custom:{}", name),
}
}
}
impl EventAction {
pub fn is_content_action(&self) -> bool {
matches!(
self,
EventAction::Content | EventAction::ContentBlockDelta
)
}
pub fn is_terminal(&self) -> bool {
matches!(
self,
EventAction::Finish | EventAction::Stop | EventAction::Error
)
}
pub fn is_ignorable(&self) -> bool {
matches!(self, EventAction::Ping)
}
pub fn from_str(s: &str) -> Self {
match s {
"content" => EventAction::Content,
"role_start" => EventAction::RoleStart,
"tool_call" => EventAction::ToolCall,
"finish" => EventAction::Finish,
"usage_update" => EventAction::UsageUpdate,
"error" => EventAction::Error,
"message_start" => EventAction::MessageStart,
"message_delta" => EventAction::MessageDelta,
"content_block_start" | "content_start" => EventAction::ContentBlockStart,
"content_block_delta" => EventAction::ContentBlockDelta,
"content_block_stop" | "content_stop" => EventAction::ContentBlockStop,
"ping" => EventAction::Ping,
"start" => EventAction::Start,
"stop" => EventAction::Stop,
other => EventAction::Custom(other.to_string()),
}
}
}
pub type TransformFn = Arc<dyn Fn(Value) -> Value + Send + Sync>;
#[derive(Clone)]
pub struct Transform(pub TransformFn);
impl fmt::Debug for Transform {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Transform(<fn>)")
}
}
impl Transform {
pub fn new<F>(f: F) -> Self
where
F: Fn(Value) -> Value + Send + Sync + 'static,
{
Transform(Arc::new(f))
}
pub fn apply(&self, value: Value) -> Value {
(self.0)(value)
}
}
#[derive(Debug, Clone)]
pub struct EventHandler {
extract_path: JsonPath,
filter_path: Option<JsonPath>,
event_type: Option<String>,
action: EventAction,
transform: Option<Transform>,
}
impl EventHandler {
pub fn builder() -> EventHandlerBuilder {
EventHandlerBuilder::new()
}
pub fn extract_path(&self) -> &JsonPath {
&self.extract_path
}
pub fn filter_path(&self) -> Option<&JsonPath> {
self.filter_path.as_ref()
}
pub fn event_type(&self) -> Option<&str> {
self.event_type.as_deref()
}
pub fn action(&self) -> &EventAction {
&self.action
}
pub fn matches(&self, event_type: Option<&str>, payload: &Value) -> bool {
if let Some(expected_type) = &self.event_type {
match event_type {
Some(actual_type) if actual_type == expected_type => {}
_ => return false,
}
}
if let Some(filter) = &self.filter_path {
if filter.navigate(payload).is_none() {
return false;
}
}
true
}
pub fn extract(&self, payload: &Value) -> Option<Value> {
let extracted = self.extract_path.navigate(payload)?.clone();
match &self.transform {
Some(transform) => Some(transform.apply(extracted)),
None => Some(extracted),
}
}
pub fn execute(&self, event_type: Option<&str>, payload: &Value) -> Option<(EventAction, Value)> {
if !self.matches(event_type, payload) {
return None;
}
self.extract(payload).map(|value| (self.action.clone(), value))
}
}
#[derive(Debug, Default)]
pub struct EventHandlerBuilder {
extract_path: Option<JsonPath>,
filter_path: Option<JsonPath>,
event_type: Option<String>,
action: EventAction,
transform: Option<Transform>,
}
impl EventHandlerBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn extract_path(mut self, path: &str) -> Self {
self.extract_path = Some(parse_json_path(path));
self
}
pub fn extract_path_raw(mut self, path: JsonPath) -> Self {
self.extract_path = Some(path);
self
}
pub fn filter_path(mut self, path: &str) -> Self {
self.filter_path = Some(parse_json_path(path));
self
}
pub fn filter_path_raw(mut self, path: JsonPath) -> Self {
self.filter_path = Some(path);
self
}
pub fn event_type(mut self, event_type: &str) -> Self {
self.event_type = Some(event_type.to_string());
self
}
pub fn action(mut self, action: EventAction) -> Self {
self.action = action;
self
}
pub fn transform<F>(mut self, f: F) -> Self
where
F: Fn(Value) -> Value + Send + Sync + 'static,
{
self.transform = Some(Transform::new(f));
self
}
pub fn build(self) -> EventHandler {
EventHandler {
extract_path: self.extract_path.expect("extract_path is required"),
filter_path: self.filter_path,
event_type: self.event_type,
action: self.action,
transform: self.transform,
}
}
pub fn try_build(self) -> Option<EventHandler> {
Some(EventHandler {
extract_path: self.extract_path?,
filter_path: self.filter_path,
event_type: self.event_type,
action: self.action,
transform: self.transform,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct EventHandlerSet {
handlers: Vec<EventHandler>,
}
impl EventHandlerSet {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, handler: EventHandler) {
self.handlers.push(handler);
}
pub fn with(mut self, handler: EventHandler) -> Self {
self.add(handler);
self
}
pub fn process(&self, event_type: Option<&str>, payload: &Value) -> Option<(EventAction, Value)> {
for handler in &self.handlers {
if let Some(result) = handler.execute(event_type, payload) {
return Some(result);
}
}
None
}
pub fn handlers(&self) -> &[EventHandler] {
&self.handlers
}
pub fn len(&self) -> usize {
self.handlers.len()
}
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
}
pub fn openai_handlers() -> EventHandlerSet {
EventHandlerSet::new()
.with(
EventHandler::builder()
.extract_path("choices[0].delta.content")
.filter_path("choices[0].delta.content")
.action(EventAction::Content)
.build(),
)
.with(
EventHandler::builder()
.extract_path("choices[0].delta.role")
.filter_path("choices[0].delta.role")
.action(EventAction::RoleStart)
.build(),
)
.with(
EventHandler::builder()
.extract_path("choices[0].finish_reason")
.filter_path("choices[0].finish_reason")
.action(EventAction::Finish)
.build(),
)
.with(
EventHandler::builder()
.extract_path("choices[0].delta.tool_calls")
.filter_path("choices[0].delta.tool_calls")
.action(EventAction::ToolCall)
.build(),
)
.with(
EventHandler::builder()
.extract_path("usage")
.filter_path("usage")
.action(EventAction::UsageUpdate)
.build(),
)
}
pub fn anthropic_handlers() -> EventHandlerSet {
EventHandlerSet::new()
.with(
EventHandler::builder()
.event_type("message_start")
.extract_path("message")
.action(EventAction::MessageStart)
.build(),
)
.with(
EventHandler::builder()
.event_type("content_block_start")
.extract_path("content_block")
.action(EventAction::ContentBlockStart)
.build(),
)
.with(
EventHandler::builder()
.event_type("content_block_delta")
.extract_path("delta.text")
.filter_path("delta.text")
.action(EventAction::Content)
.build(),
)
.with(
EventHandler::builder()
.event_type("content_block_delta")
.extract_path("delta.partial_json")
.filter_path("delta.partial_json")
.action(EventAction::ToolCall)
.build(),
)
.with(
EventHandler::builder()
.event_type("content_block_stop")
.extract_path("index")
.action(EventAction::ContentBlockStop)
.build(),
)
.with(
EventHandler::builder()
.event_type("message_delta")
.extract_path("delta")
.action(EventAction::MessageDelta)
.build(),
)
.with(
EventHandler::builder()
.event_type("message_stop")
.extract_path("type")
.action(EventAction::Stop)
.build(),
)
.with(
EventHandler::builder()
.event_type("error")
.extract_path("error")
.action(EventAction::Error)
.build(),
)
.with(
EventHandler::builder()
.event_type("ping")
.extract_path("type")
.action(EventAction::Ping)
.build(),
)
}
pub fn parse_json_path(path: &str) -> JsonPath {
let mut segments = Vec::new();
for part in path.split('.') {
if part.is_empty() {
continue;
}
if let Some(bracket_pos) = part.find('[') {
let key = &part[..bracket_pos];
if !key.is_empty() {
segments.push(JsonPathSegment::Key(leak_str(key)));
}
if let Some(close_pos) = part.find(']') {
let index_str = &part[bracket_pos + 1..close_pos];
if let Ok(index) = index_str.parse::<usize>() {
segments.push(JsonPathSegment::Index(index));
}
}
} else {
segments.push(JsonPathSegment::Key(leak_str(part)));
}
}
JsonPath(segments)
}
fn leak_str(s: &str) -> &'static str {
Box::leak(s.to_string().into_boxed_str())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventHandlerConfig {
pub extract: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub event_type: Option<String>,
pub action: String,
}
impl From<EventHandlerConfig> for EventHandler {
fn from(config: EventHandlerConfig) -> Self {
let mut builder = EventHandler::builder()
.extract_path(&config.extract)
.action(EventAction::from_str(&config.action));
if let Some(filter) = config.filter {
builder = builder.filter_path(&filter);
}
if let Some(event_type) = config.event_type {
builder = builder.event_type(&event_type);
}
builder.build()
}
}
impl From<&EventHandler> for EventHandlerConfig {
fn from(handler: &EventHandler) -> Self {
EventHandlerConfig {
extract: json_path_to_string(handler.extract_path()),
filter: handler.filter_path().map(json_path_to_string),
event_type: handler.event_type().map(String::from),
action: handler.action().to_string(),
}
}
}
fn json_path_to_string(path: &JsonPath) -> String {
let mut result = String::new();
for (i, segment) in path.0.iter().enumerate() {
match segment {
JsonPathSegment::Key(key) => {
if i > 0 && !result.ends_with(']') {
result.push('.');
}
result.push_str(key);
}
JsonPathSegment::Index(idx) => {
result.push('[');
result.push_str(&idx.to_string());
result.push(']');
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
mod event_action_tests {
use super::*;
#[test]
fn test_event_action_serialization() {
assert_eq!(
serde_json::to_string(&EventAction::Content).unwrap(),
"\"content\""
);
assert_eq!(
serde_json::to_string(&EventAction::ToolCall).unwrap(),
"\"tool_call\""
);
assert_eq!(
serde_json::to_string(&EventAction::UsageUpdate).unwrap(),
"\"usage_update\""
);
assert_eq!(
serde_json::to_string(&EventAction::Custom("my_action".to_string())).unwrap(),
"{\"custom\":\"my_action\"}"
);
}
#[test]
fn test_event_action_deserialization() {
assert_eq!(
serde_json::from_str::<EventAction>("\"content\"").unwrap(),
EventAction::Content
);
assert_eq!(
serde_json::from_str::<EventAction>("\"message_start\"").unwrap(),
EventAction::MessageStart
);
assert_eq!(
serde_json::from_str::<EventAction>("{\"custom\":\"foo\"}").unwrap(),
EventAction::Custom("foo".to_string())
);
}
#[test]
fn test_event_action_from_str() {
assert_eq!(EventAction::from_str("content"), EventAction::Content);
assert_eq!(EventAction::from_str("tool_call"), EventAction::ToolCall);
assert_eq!(EventAction::from_str("message_start"), EventAction::MessageStart);
assert_eq!(EventAction::from_str("content_start"), EventAction::ContentBlockStart);
assert_eq!(
EventAction::from_str("my_custom"),
EventAction::Custom("my_custom".to_string())
);
}
#[test]
fn test_event_action_display() {
assert_eq!(EventAction::Content.to_string(), "content");
assert_eq!(EventAction::MessageDelta.to_string(), "message_delta");
assert_eq!(
EventAction::Custom("test".to_string()).to_string(),
"custom:test"
);
}
#[test]
fn test_event_action_predicates() {
assert!(EventAction::Content.is_content_action());
assert!(EventAction::ContentBlockDelta.is_content_action());
assert!(!EventAction::ToolCall.is_content_action());
assert!(EventAction::Finish.is_terminal());
assert!(EventAction::Stop.is_terminal());
assert!(EventAction::Error.is_terminal());
assert!(!EventAction::Content.is_terminal());
assert!(EventAction::Ping.is_ignorable());
assert!(!EventAction::Content.is_ignorable());
}
}
mod json_path_tests {
use super::*;
#[test]
fn test_parse_simple_path() {
let path = parse_json_path("delta.text");
assert_eq!(path.0.len(), 2);
let payload = json!({"delta": {"text": "hello"}});
let result = path.navigate(&payload);
assert_eq!(result, Some(&json!("hello")));
}
#[test]
fn test_parse_array_path() {
let path = parse_json_path("choices[0].delta.content");
assert_eq!(path.0.len(), 4);
let payload = json!({
"choices": [{"delta": {"content": "token"}}]
});
let result = path.navigate(&payload);
assert_eq!(result, Some(&json!("token")));
}
#[test]
fn test_parse_nested_array_path() {
let path = parse_json_path("data[0].items[2].value");
let payload = json!({
"data": [
{"items": ["a", "b", {"value": "found"}]}
]
});
let result = path.navigate(&payload);
assert_eq!(result, Some(&json!("found")));
}
#[test]
fn test_path_navigation_missing() {
let path = parse_json_path("foo.bar.baz");
let payload = json!({"foo": {"other": 1}});
assert_eq!(path.navigate(&payload), None);
}
#[test]
fn test_json_path_to_string() {
let path = parse_json_path("choices[0].delta.content");
let string = json_path_to_string(&path);
assert_eq!(string, "choices[0].delta.content");
}
}
mod event_handler_tests {
use super::*;
#[test]
fn test_builder_basic() {
let handler = EventHandler::builder()
.extract_path("delta.text")
.action(EventAction::Content)
.build();
assert_eq!(handler.action(), &EventAction::Content);
assert!(handler.filter_path().is_none());
assert!(handler.event_type().is_none());
}
#[test]
fn test_builder_with_filter() {
let handler = EventHandler::builder()
.extract_path("choices[0].delta.content")
.filter_path("choices[0].delta.content")
.action(EventAction::Content)
.build();
assert!(handler.filter_path().is_some());
}
#[test]
fn test_builder_with_event_type() {
let handler = EventHandler::builder()
.event_type("content_block_delta")
.extract_path("delta.text")
.action(EventAction::Content)
.build();
assert_eq!(handler.event_type(), Some("content_block_delta"));
}
#[test]
fn test_builder_with_transform() {
let handler = EventHandler::builder()
.extract_path("value")
.action(EventAction::Content)
.transform(|v| {
if let Some(s) = v.as_str() {
Value::String(s.to_uppercase())
} else {
v
}
})
.build();
let payload = json!({"value": "hello"});
let extracted = handler.extract(&payload);
assert_eq!(extracted, Some(json!("HELLO")));
}
#[test]
#[should_panic(expected = "extract_path is required")]
fn test_builder_missing_path() {
EventHandler::builder()
.action(EventAction::Content)
.build();
}
#[test]
fn test_try_build_missing_path() {
let result = EventHandler::builder()
.action(EventAction::Content)
.try_build();
assert!(result.is_none());
}
#[test]
fn test_handler_matches_no_filters() {
let handler = EventHandler::builder()
.extract_path("content")
.action(EventAction::Content)
.build();
let payload = json!({"content": "test"});
assert!(handler.matches(None, &payload));
assert!(handler.matches(Some("any_event"), &payload));
}
#[test]
fn test_handler_matches_event_type() {
let handler = EventHandler::builder()
.event_type("content_block_delta")
.extract_path("delta.text")
.action(EventAction::Content)
.build();
let payload = json!({"delta": {"text": "hi"}});
assert!(handler.matches(Some("content_block_delta"), &payload));
assert!(!handler.matches(Some("message_start"), &payload));
assert!(!handler.matches(None, &payload));
}
#[test]
fn test_handler_matches_filter_path() {
let handler = EventHandler::builder()
.extract_path("choices[0].delta.content")
.filter_path("choices[0].delta.content")
.action(EventAction::Content)
.build();
let matching = json!({"choices": [{"delta": {"content": "token"}}]});
let non_matching = json!({"choices": [{"delta": {"role": "assistant"}}]});
assert!(handler.matches(None, &matching));
assert!(!handler.matches(None, &non_matching));
}
#[test]
fn test_handler_extract() {
let handler = EventHandler::builder()
.extract_path("choices[0].delta.content")
.action(EventAction::Content)
.build();
let payload = json!({"choices": [{"delta": {"content": "hello world"}}]});
let extracted = handler.extract(&payload);
assert_eq!(extracted, Some(json!("hello world")));
}
#[test]
fn test_handler_extract_missing() {
let handler = EventHandler::builder()
.extract_path("nonexistent.path")
.action(EventAction::Content)
.build();
let payload = json!({"other": "data"});
let extracted = handler.extract(&payload);
assert_eq!(extracted, None);
}
#[test]
fn test_handler_execute() {
let handler = EventHandler::builder()
.event_type("content_block_delta")
.extract_path("delta.text")
.filter_path("delta.text")
.action(EventAction::Content)
.build();
let payload = json!({"delta": {"text": "token"}});
let result = handler.execute(Some("content_block_delta"), &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::Content);
assert_eq!(value, json!("token"));
let result = handler.execute(Some("message_start"), &payload);
assert!(result.is_none());
}
}
mod event_handler_set_tests {
use super::*;
#[test]
fn test_empty_set() {
let set = EventHandlerSet::new();
assert!(set.is_empty());
assert_eq!(set.len(), 0);
}
#[test]
fn test_add_handlers() {
let mut set = EventHandlerSet::new();
set.add(
EventHandler::builder()
.extract_path("content")
.action(EventAction::Content)
.build(),
);
assert_eq!(set.len(), 1);
}
#[test]
fn test_with_chaining() {
let set = EventHandlerSet::new()
.with(
EventHandler::builder()
.extract_path("a")
.action(EventAction::Content)
.build(),
)
.with(
EventHandler::builder()
.extract_path("b")
.action(EventAction::ToolCall)
.build(),
);
assert_eq!(set.len(), 2);
}
#[test]
fn test_process_first_match_wins() {
let set = EventHandlerSet::new()
.with(
EventHandler::builder()
.extract_path("content")
.filter_path("content")
.action(EventAction::Content)
.build(),
)
.with(
EventHandler::builder()
.extract_path("role")
.filter_path("role")
.action(EventAction::RoleStart)
.build(),
);
let payload = json!({"content": "test", "role": "assistant"});
let result = set.process(None, &payload);
assert!(result.is_some());
let (action, _) = result.unwrap();
assert_eq!(action, EventAction::Content);
let payload = json!({"role": "assistant"});
let result = set.process(None, &payload);
assert!(result.is_some());
let (action, _) = result.unwrap();
assert_eq!(action, EventAction::RoleStart);
let payload = json!({"other": "data"});
let result = set.process(None, &payload);
assert!(result.is_none());
}
}
mod provider_handler_tests {
use super::*;
#[test]
fn test_openai_content_handler() {
let handlers = openai_handlers();
let payload = json!({
"choices": [{
"delta": {"content": "Hello"}
}]
});
let result = handlers.process(None, &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::Content);
assert_eq!(value, json!("Hello"));
}
#[test]
fn test_openai_role_handler() {
let handlers = openai_handlers();
let payload = json!({
"choices": [{
"delta": {"role": "assistant"}
}]
});
let result = handlers.process(None, &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::RoleStart);
assert_eq!(value, json!("assistant"));
}
#[test]
fn test_openai_finish_handler() {
let handlers = openai_handlers();
let payload = json!({
"choices": [{
"delta": {},
"finish_reason": "stop"
}]
});
let result = handlers.process(None, &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::Finish);
assert_eq!(value, json!("stop"));
}
#[test]
fn test_openai_usage_handler() {
let handlers = openai_handlers();
let payload = json!({
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30
}
});
let result = handlers.process(None, &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::UsageUpdate);
assert_eq!(value["prompt_tokens"], 10);
}
#[test]
fn test_anthropic_message_start() {
let handlers = anthropic_handlers();
let payload = json!({
"type": "message_start",
"message": {
"id": "msg_123",
"model": "claude-3-opus",
"usage": {"input_tokens": 100}
}
});
let result = handlers.process(Some("message_start"), &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::MessageStart);
assert_eq!(value["id"], "msg_123");
}
#[test]
fn test_anthropic_content_delta() {
let handlers = anthropic_handlers();
let payload = json!({
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hello"}
});
let result = handlers.process(Some("content_block_delta"), &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::Content);
assert_eq!(value, json!("Hello"));
}
#[test]
fn test_anthropic_tool_delta() {
let handlers = anthropic_handlers();
let payload = json!({
"type": "content_block_delta",
"index": 1,
"delta": {"type": "input_json_delta", "partial_json": "{\"key\":"}
});
let result = handlers.process(Some("content_block_delta"), &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::ToolCall);
assert_eq!(value, json!("{\"key\":"));
}
#[test]
fn test_anthropic_message_stop() {
let handlers = anthropic_handlers();
let payload = json!({"type": "message_stop"});
let result = handlers.process(Some("message_stop"), &payload);
assert!(result.is_some());
let (action, _) = result.unwrap();
assert_eq!(action, EventAction::Stop);
}
#[test]
fn test_anthropic_error() {
let handlers = anthropic_handlers();
let payload = json!({
"type": "error",
"error": {
"type": "rate_limit_error",
"message": "Too many requests"
}
});
let result = handlers.process(Some("error"), &payload);
assert!(result.is_some());
let (action, value) = result.unwrap();
assert_eq!(action, EventAction::Error);
assert_eq!(value["message"], "Too many requests");
}
#[test]
fn test_anthropic_ping() {
let handlers = anthropic_handlers();
let payload = json!({"type": "ping"});
let result = handlers.process(Some("ping"), &payload);
assert!(result.is_some());
let (action, _) = result.unwrap();
assert_eq!(action, EventAction::Ping);
assert!(action.is_ignorable());
}
}
mod config_serialization_tests {
use super::*;
#[test]
fn test_config_to_handler() {
let config = EventHandlerConfig {
extract: "choices[0].delta.content".to_string(),
filter: Some("choices[0].delta.content".to_string()),
event_type: None,
action: "content".to_string(),
};
let handler: EventHandler = config.into();
assert_eq!(handler.action(), &EventAction::Content);
assert!(handler.filter_path().is_some());
}
#[test]
fn test_handler_to_config() {
let handler = EventHandler::builder()
.event_type("content_block_delta")
.extract_path("delta.text")
.filter_path("delta.text")
.action(EventAction::Content)
.build();
let config: EventHandlerConfig = (&handler).into();
assert_eq!(config.extract, "delta.text");
assert_eq!(config.filter, Some("delta.text".to_string()));
assert_eq!(config.event_type, Some("content_block_delta".to_string()));
assert_eq!(config.action, "content");
}
#[test]
fn test_config_yaml_roundtrip() {
let yaml = r#"
extract: choices[0].delta.content
filter: choices[0].delta.content
action: content
"#;
let config: EventHandlerConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.extract, "choices[0].delta.content");
assert_eq!(config.action, "content");
let yaml_out = serde_yaml::to_string(&config).unwrap();
let config2: EventHandlerConfig = serde_yaml::from_str(&yaml_out).unwrap();
assert_eq!(config.extract, config2.extract);
}
#[test]
fn test_config_with_event_type() {
let yaml = r#"
type: content_block_delta
extract: delta.text
action: content
"#;
let config: EventHandlerConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.event_type, Some("content_block_delta".to_string()));
}
}
}