use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum EndCondition {
RawLine {
raw_line: String,
},
FieldEquals {
field: String,
equals: Value,
},
FieldNotNull {
field: String,
not_null: bool,
},
FieldExists {
field: String,
exists: bool,
},
EventType {
event_type: String,
},
}
impl EndCondition {
pub fn is_met(&self, raw_line: &str, parsed: Option<&Value>, event_type: Option<&str>) -> bool {
match self {
EndCondition::RawLine { raw_line: expected } => {
raw_line.trim() == expected
}
EndCondition::FieldEquals { field, equals } => {
parsed
.and_then(|json| navigate_json_path(json, field))
.is_some_and(|v| v == equals)
}
EndCondition::FieldNotNull { field, not_null } => {
if !*not_null {
return false;
}
parsed
.and_then(|json| navigate_json_path(json, field))
.is_some_and(|v| !v.is_null())
}
EndCondition::FieldExists { field, exists } => {
if !*exists {
return false;
}
parsed
.and_then(|json| navigate_json_path(json, field))
.is_some()
}
EndCondition::EventType { event_type: expected } => {
event_type.is_some_and(|et| et == expected)
}
}
}
pub fn description(&self) -> String {
match self {
EndCondition::RawLine { raw_line } => {
format!("raw line equals \"{}\"", raw_line)
}
EndCondition::FieldEquals { field, equals } => {
format!("field {} equals {}", field, equals)
}
EndCondition::FieldNotNull { field, .. } => {
format!("field {} is not null", field)
}
EndCondition::FieldExists { field, .. } => {
format!("field {} exists", field)
}
EndCondition::EventType { event_type } => {
format!("event type is \"{}\"", event_type)
}
}
}
}
fn navigate_json_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
let mut current = value;
let segments = parse_path_segments(path);
for segment in segments {
current = match segment {
PathSegment::Key(key) => current.get(key)?,
PathSegment::Index(idx) => current.get(idx)?,
};
}
Some(current)
}
#[derive(Debug, Clone)]
enum PathSegment<'a> {
Key(&'a str),
Index(usize),
}
fn parse_path_segments(path: &str) -> Vec<PathSegment<'_>> {
let mut segments = Vec::new();
let mut remaining = path;
while !remaining.is_empty() {
remaining = remaining.trim_start_matches('.');
if remaining.is_empty() {
break;
}
if remaining.starts_with('[') {
if let Some(end) = remaining.find(']') {
let idx_str = &remaining[1..end];
if let Ok(idx) = idx_str.parse::<usize>() {
segments.push(PathSegment::Index(idx));
}
remaining = &remaining[end + 1..];
continue;
}
}
let key_end = remaining
.find(|c| c == '.' || c == '[')
.unwrap_or(remaining.len());
if key_end > 0 {
let key = &remaining[..key_end];
segments.push(PathSegment::Key(key));
remaining = &remaining[key_end..];
} else {
break;
}
}
segments
}
pub struct EndConditionBuilder;
impl EndConditionBuilder {
pub fn raw_line(line: impl Into<String>) -> EndCondition {
EndCondition::RawLine {
raw_line: line.into(),
}
}
pub fn field_not_null(path: impl Into<String>) -> EndCondition {
EndCondition::FieldNotNull {
field: path.into(),
not_null: true,
}
}
pub fn field_exists(path: impl Into<String>) -> EndCondition {
EndCondition::FieldExists {
field: path.into(),
exists: true,
}
}
pub fn field_equals(path: impl Into<String>, value: impl Into<Value>) -> EndCondition {
EndCondition::FieldEquals {
field: path.into(),
equals: value.into(),
}
}
pub fn event_type(typ: impl Into<String>) -> EndCondition {
EndCondition::EventType {
event_type: typ.into(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EndConditions(pub Vec<EndCondition>);
impl EndConditions {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn add(&mut self, condition: EndCondition) {
self.0.push(condition);
}
pub fn any_met(&self, raw_line: &str, parsed: Option<&Value>, event_type: Option<&str>) -> bool {
self.0.iter().any(|c| c.is_met(raw_line, parsed, event_type))
}
pub fn which_met(&self, raw_line: &str, parsed: Option<&Value>, event_type: Option<&str>) -> Option<&EndCondition> {
self.0.iter().find(|c| c.is_met(raw_line, parsed, event_type))
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl FromIterator<EndCondition> for EndConditions {
fn from_iter<I: IntoIterator<Item = EndCondition>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
impl IntoIterator for EndConditions {
type Item = EndCondition;
type IntoIter = std::vec::IntoIter<EndCondition>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a EndConditions {
type Item = &'a EndCondition;
type IntoIter = std::slice::Iter<'a, EndCondition>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_simple_path() {
let segments = parse_path_segments("foo");
assert_eq!(segments.len(), 1);
assert!(matches!(segments[0], PathSegment::Key("foo")));
}
#[test]
fn test_parse_dotted_path() {
let segments = parse_path_segments("foo.bar.baz");
assert_eq!(segments.len(), 3);
assert!(matches!(segments[0], PathSegment::Key("foo")));
assert!(matches!(segments[1], PathSegment::Key("bar")));
assert!(matches!(segments[2], PathSegment::Key("baz")));
}
#[test]
fn test_parse_array_index() {
let segments = parse_path_segments("arr[0]");
assert_eq!(segments.len(), 2);
assert!(matches!(segments[0], PathSegment::Key("arr")));
assert!(matches!(segments[1], PathSegment::Index(0)));
}
#[test]
fn test_parse_complex_path() {
let segments = parse_path_segments("choices[0].delta.content");
assert_eq!(segments.len(), 4);
assert!(matches!(segments[0], PathSegment::Key("choices")));
assert!(matches!(segments[1], PathSegment::Index(0)));
assert!(matches!(segments[2], PathSegment::Key("delta")));
assert!(matches!(segments[3], PathSegment::Key("content")));
}
#[test]
fn test_parse_nested_arrays() {
let segments = parse_path_segments("arr[0][1]");
assert_eq!(segments.len(), 3);
assert!(matches!(segments[0], PathSegment::Key("arr")));
assert!(matches!(segments[1], PathSegment::Index(0)));
assert!(matches!(segments[2], PathSegment::Index(1)));
}
#[test]
fn test_navigate_simple_key() {
let json = json!({"foo": "bar"});
let result = navigate_json_path(&json, "foo");
assert_eq!(result, Some(&json!("bar")));
}
#[test]
fn test_navigate_nested_keys() {
let json = json!({"foo": {"bar": {"baz": 42}}});
let result = navigate_json_path(&json, "foo.bar.baz");
assert_eq!(result, Some(&json!(42)));
}
#[test]
fn test_navigate_array_index() {
let json = json!({"arr": [1, 2, 3]});
let result = navigate_json_path(&json, "arr[1]");
assert_eq!(result, Some(&json!(2)));
}
#[test]
fn test_navigate_openai_path() {
let json = json!({
"choices": [{
"delta": {"content": "Hello"},
"finish_reason": null
}]
});
let result = navigate_json_path(&json, "choices[0].delta.content");
assert_eq!(result, Some(&json!("Hello")));
}
#[test]
fn test_navigate_missing_key() {
let json = json!({"foo": "bar"});
let result = navigate_json_path(&json, "baz");
assert_eq!(result, None);
}
#[test]
fn test_navigate_out_of_bounds() {
let json = json!({"arr": [1, 2]});
let result = navigate_json_path(&json, "arr[5]");
assert_eq!(result, None);
}
#[test]
fn test_raw_line_exact_match() {
let condition = EndConditionBuilder::raw_line("[DONE]");
assert!(condition.is_met("[DONE]", None, None));
}
#[test]
fn test_raw_line_trimmed_match() {
let condition = EndConditionBuilder::raw_line("[DONE]");
assert!(condition.is_met(" [DONE] ", None, None));
}
#[test]
fn test_raw_line_no_match() {
let condition = EndConditionBuilder::raw_line("[DONE]");
assert!(!condition.is_met("[FINISHED]", None, None));
}
#[test]
fn test_field_equals_string() {
let condition = EndConditionBuilder::field_equals("type", "message_stop");
let json = json!({"type": "message_stop"});
assert!(condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_equals_number() {
let condition = EndConditionBuilder::field_equals("status", 200);
let json = json!({"status": 200});
assert!(condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_equals_nested() {
let condition = EndConditionBuilder::field_equals("response.type", "complete");
let json = json!({"response": {"type": "complete"}});
assert!(condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_equals_wrong_value() {
let condition = EndConditionBuilder::field_equals("type", "message_stop");
let json = json!({"type": "content_block_delta"});
assert!(!condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_equals_missing_field() {
let condition = EndConditionBuilder::field_equals("type", "message_stop");
let json = json!({"other": "value"});
assert!(!condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_not_null_with_value() {
let condition = EndConditionBuilder::field_not_null("finish_reason");
let json = json!({"finish_reason": "stop"});
assert!(condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_not_null_with_null() {
let condition = EndConditionBuilder::field_not_null("finish_reason");
let json = json!({"finish_reason": null});
assert!(!condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_not_null_missing() {
let condition = EndConditionBuilder::field_not_null("finish_reason");
let json = json!({"other": "value"});
assert!(!condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_not_null_nested() {
let condition = EndConditionBuilder::field_not_null("choices[0].finish_reason");
let json = json!({
"choices": [{"finish_reason": "length"}]
});
assert!(condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_not_null_nested_null() {
let condition = EndConditionBuilder::field_not_null("choices[0].finish_reason");
let json = json!({
"choices": [{"finish_reason": null}]
});
assert!(!condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_exists_present() {
let condition = EndConditionBuilder::field_exists("error");
let json = json!({"error": {"message": "test"}});
assert!(condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_exists_null_value() {
let condition = EndConditionBuilder::field_exists("error");
let json = json!({"error": null});
assert!(condition.is_met("", Some(&json), None));
}
#[test]
fn test_field_exists_missing() {
let condition = EndConditionBuilder::field_exists("error");
let json = json!({"success": true});
assert!(!condition.is_met("", Some(&json), None));
}
#[test]
fn test_event_type_match() {
let condition = EndConditionBuilder::event_type("message_stop");
assert!(condition.is_met("", None, Some("message_stop")));
}
#[test]
fn test_event_type_no_match() {
let condition = EndConditionBuilder::event_type("message_stop");
assert!(!condition.is_met("", None, Some("content_block_delta")));
}
#[test]
fn test_event_type_none_provided() {
let condition = EndConditionBuilder::event_type("message_stop");
assert!(!condition.is_met("", None, None));
}
#[test]
fn test_deserialize_raw_line() {
let yaml = r#"raw_line: "[DONE]""#;
let condition: EndCondition = serde_yaml::from_str(yaml).unwrap();
assert!(matches!(condition, EndCondition::RawLine { raw_line } if raw_line == "[DONE]"));
}
#[test]
fn test_deserialize_field_equals() {
let yaml = r#"
field: type
equals: message_stop
"#;
let condition: EndCondition = serde_yaml::from_str(yaml).unwrap();
assert!(condition.is_met("", Some(&json!({"type": "message_stop"})), None));
}
#[test]
fn test_deserialize_field_not_null() {
let yaml = r#"
field: choices[0].finish_reason
not_null: true
"#;
let condition: EndCondition = serde_yaml::from_str(yaml).unwrap();
assert!(condition.is_met("", Some(&json!({"choices": [{"finish_reason": "stop"}]})), None));
}
#[test]
fn test_deserialize_field_exists() {
let yaml = r#"
field: error
exists: true
"#;
let condition: EndCondition = serde_yaml::from_str(yaml).unwrap();
assert!(condition.is_met("", Some(&json!({"error": null})), None));
}
#[test]
fn test_deserialize_event_type() {
let yaml = r#"event_type: message_stop"#;
let condition: EndCondition = serde_yaml::from_str(yaml).unwrap();
assert!(condition.is_met("", None, Some("message_stop")));
}
#[test]
fn test_deserialize_openai_conditions() {
let yaml = r#"
- raw_line: "[DONE]"
- field: choices[0].finish_reason
not_null: true
"#;
let conditions: Vec<EndCondition> = serde_yaml::from_str(yaml).unwrap();
assert_eq!(conditions.len(), 2);
assert!(conditions[0].is_met("[DONE]", None, None));
let json = json!({"choices": [{"finish_reason": "stop"}]});
assert!(conditions[1].is_met("", Some(&json), None));
}
#[test]
fn test_deserialize_anthropic_conditions() {
let yaml = r#"
- field: type
equals: message_stop
- field: error
exists: true
"#;
let conditions: Vec<EndCondition> = serde_yaml::from_str(yaml).unwrap();
assert_eq!(conditions.len(), 2);
let json = json!({"type": "message_stop"});
assert!(conditions[0].is_met("", Some(&json), None));
let json = json!({"error": {"message": "test"}});
assert!(conditions[1].is_met("", Some(&json), None));
}
#[test]
fn test_deserialize_google_conditions() {
let yaml = r#"
- field: candidates[0].finishReason
not_null: true
- field: error
exists: true
"#;
let conditions: Vec<EndCondition> = serde_yaml::from_str(yaml).unwrap();
assert_eq!(conditions.len(), 2);
let json = json!({"candidates": [{"finishReason": "STOP"}]});
assert!(conditions[0].is_met("", Some(&json), None));
}
#[test]
fn test_end_conditions_any_met() {
let conditions = EndConditions(vec![
EndConditionBuilder::raw_line("[DONE]"),
EndConditionBuilder::field_not_null("finish_reason"),
]);
assert!(conditions.any_met("[DONE]", None, None));
let json = json!({"finish_reason": "stop"});
assert!(conditions.any_met("other", Some(&json), None));
let json = json!({"finish_reason": null});
assert!(!conditions.any_met("other", Some(&json), None));
}
#[test]
fn test_end_conditions_which_met() {
let conditions = EndConditions(vec![
EndConditionBuilder::raw_line("[DONE]"),
EndConditionBuilder::field_not_null("finish_reason"),
]);
let met = conditions.which_met("[DONE]", None, None);
assert!(met.is_some());
assert!(matches!(met.unwrap(), EndCondition::RawLine { .. }));
}
#[test]
fn test_end_conditions_empty() {
let conditions = EndConditions::new();
assert!(conditions.is_empty());
assert!(!conditions.any_met("[DONE]", None, None));
}
#[test]
fn test_openai_stream_end() {
let yaml = r#"
- raw_line: "[DONE]"
- field: choices[0].finish_reason
not_null: true
"#;
let conditions: EndConditions = EndConditions(serde_yaml::from_str(yaml).unwrap());
assert!(conditions.any_met("[DONE]", None, None));
let json = json!({
"id": "chatcmpl-123",
"choices": [{
"index": 0,
"delta": {},
"finish_reason": "stop"
}]
});
assert!(conditions.any_met("", Some(&json), None));
let json = json!({
"id": "chatcmpl-123",
"choices": [{
"index": 0,
"delta": {"content": "Hello"},
"finish_reason": null
}]
});
assert!(!conditions.any_met("", Some(&json), None));
}
#[test]
fn test_anthropic_stream_end() {
let yaml = r#"
- field: type
equals: message_stop
- field: error
exists: true
"#;
let conditions: EndConditions = EndConditions(serde_yaml::from_str(yaml).unwrap());
let json = json!({"type": "message_stop"});
assert!(conditions.any_met("", Some(&json), None));
let json = json!({
"type": "error",
"error": {"type": "rate_limit", "message": "Too many requests"}
});
assert!(conditions.any_met("", Some(&json), None));
let json = json!({
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Hello"}
});
assert!(!conditions.any_met("", Some(&json), None));
}
#[test]
fn test_google_stream_end() {
let yaml = r#"
- field: candidates[0].finishReason
not_null: true
- field: error
exists: true
"#;
let conditions: EndConditions = EndConditions(serde_yaml::from_str(yaml).unwrap());
let json = json!({
"candidates": [{
"content": {"parts": [{"text": "Hello"}]},
"finishReason": "STOP"
}]
});
assert!(conditions.any_met("", Some(&json), None));
let json = json!({
"error": {
"code": 429,
"message": "Resource exhausted"
}
});
assert!(conditions.any_met("", Some(&json), None));
let json = json!({
"candidates": [{
"content": {"parts": [{"text": "Hello"}]}
}]
});
assert!(!conditions.any_met("", Some(&json), None));
}
#[test]
fn test_descriptions() {
let conditions = vec![
EndConditionBuilder::raw_line("[DONE]"),
EndConditionBuilder::field_equals("type", "stop"),
EndConditionBuilder::field_not_null("finish_reason"),
EndConditionBuilder::field_exists("error"),
EndConditionBuilder::event_type("message_stop"),
];
assert_eq!(conditions[0].description(), "raw line equals \"[DONE]\"");
assert_eq!(conditions[1].description(), "field type equals \"stop\"");
assert_eq!(conditions[2].description(), "field finish_reason is not null");
assert_eq!(conditions[3].description(), "field error exists");
assert_eq!(conditions[4].description(), "event type is \"message_stop\"");
}
#[test]
fn test_serialize_deserialize_round_trip() {
let original = vec![
EndConditionBuilder::raw_line("[DONE]"),
EndConditionBuilder::field_equals("type", "message_stop"),
EndConditionBuilder::field_not_null("choices[0].finish_reason"),
EndConditionBuilder::field_exists("error"),
EndConditionBuilder::event_type("message_stop"),
];
let yaml = serde_yaml::to_string(&original).unwrap();
let deserialized: Vec<EndCondition> = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(original.len(), deserialized.len());
assert!(deserialized[0].is_met("[DONE]", None, None));
assert!(deserialized[1].is_met("", Some(&json!({"type": "message_stop"})), None));
assert!(deserialized[2].is_met("", Some(&json!({"choices": [{"finish_reason": "stop"}]})), None));
assert!(deserialized[3].is_met("", Some(&json!({"error": null})), None));
assert!(deserialized[4].is_met("", None, Some("message_stop")));
}
}