use crate::serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct SourceRow {
pub urn: String,
pub payload: String,
}
#[derive(Debug, Clone)]
pub struct ValidationWarning {
pub kind: String,
pub detail: String,
}
#[derive(Debug, Clone)]
pub struct AuditSummary {
pub provider: String,
pub model: String,
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub cache_hit: bool,
}
fn obj(entries: &[(&str, Value)]) -> Value {
let mut map = Map::new();
for (k, v) in entries {
map.insert((*k).to_string(), v.clone());
}
Value::Object(map)
}
fn source_row_value(row: &SourceRow) -> Value {
obj(&[
("payload", Value::String(row.payload.clone())),
("urn", Value::String(row.urn.clone())),
])
}
fn warning_value(w: &ValidationWarning) -> Value {
obj(&[
("detail", Value::String(w.detail.clone())),
("kind", Value::String(w.kind.clone())),
])
}
fn audit_value(a: &AuditSummary) -> Value {
obj(&[
("cache_hit", Value::Bool(a.cache_hit)),
(
"completion_tokens",
Value::Number(a.completion_tokens as f64),
),
("model", Value::String(a.model.clone())),
("prompt_tokens", Value::Number(a.prompt_tokens as f64)),
("provider", Value::String(a.provider.clone())),
])
}
#[derive(Debug, Clone)]
pub enum Frame {
Sources { sources_flat: Vec<SourceRow> },
AnswerToken { text: String },
Validation {
ok: bool,
warnings: Vec<ValidationWarning>,
audit: AuditSummary,
},
Error { code: u16, message: String },
}
pub mod event {
pub const SOURCES: &str = "sources";
pub const ANSWER_TOKEN: &str = "answer_token";
pub const VALIDATION: &str = "validation";
pub const ERROR: &str = "error";
}
impl Frame {
fn event_name(&self) -> &'static str {
match self {
Frame::Sources { .. } => event::SOURCES,
Frame::AnswerToken { .. } => event::ANSWER_TOKEN,
Frame::Validation { .. } => event::VALIDATION,
Frame::Error { .. } => event::ERROR,
}
}
fn payload_json(&self) -> String {
let value = match self {
Frame::Sources { sources_flat } => obj(&[(
"sources_flat",
Value::Array(sources_flat.iter().map(source_row_value).collect()),
)]),
Frame::AnswerToken { text } => obj(&[("text", Value::String(text.clone()))]),
Frame::Validation {
ok,
warnings,
audit,
} => obj(&[
("audit", audit_value(audit)),
("ok", Value::Bool(*ok)),
(
"warnings",
Value::Array(warnings.iter().map(warning_value).collect()),
),
]),
Frame::Error { code, message } => obj(&[
("code", Value::Number(*code as f64)),
("message", Value::String(message.clone())),
]),
};
value.to_string_compact()
}
}
pub fn encode(frame: &Frame) -> String {
let event = frame.event_name();
let payload = frame.payload_json();
let mut out = String::with_capacity(event.len() + payload.len() + 16);
out.push_str("event: ");
out.push_str(event);
out.push('\n');
for line in payload.split('\n') {
out.push_str("data: ");
out.push_str(line);
out.push('\n');
}
out.push('\n');
out
}
#[cfg(test)]
mod tests {
use super::*;
fn audit_fixture() -> AuditSummary {
AuditSummary {
provider: "openai".to_string(),
model: "gpt-4o-mini".to_string(),
prompt_tokens: 123,
completion_tokens: 45,
cache_hit: false,
}
}
#[test]
fn event_names_pinned() {
assert_eq!(event::SOURCES, "sources");
assert_eq!(event::ANSWER_TOKEN, "answer_token");
assert_eq!(event::VALIDATION, "validation");
assert_eq!(event::ERROR, "error");
}
#[test]
fn encodes_sources_frame_with_event_and_terminator() {
let frame = Frame::Sources {
sources_flat: vec![SourceRow {
urn: "urn:reddb:row:1".to_string(),
payload: "{\"k\":\"v\"}".to_string(),
}],
};
let out = encode(&frame);
assert!(out.starts_with("event: sources\n"));
assert!(out.ends_with("\n\n"));
assert!(out.contains("data: {"));
assert!(out.contains("\"urn\":\"urn:reddb:row:1\""));
}
#[test]
fn encodes_answer_token_frame_with_text_field() {
let frame = Frame::AnswerToken {
text: "hello".to_string(),
};
let out = encode(&frame);
assert_eq!(out, "event: answer_token\ndata: {\"text\":\"hello\"}\n\n");
}
#[test]
fn answer_token_escapes_quotes_and_backslashes() {
let frame = Frame::AnswerToken {
text: "a\"b\\c".to_string(),
};
let out = encode(&frame);
assert!(out.contains(r#"\"b\\c"#));
assert!(out.ends_with("\n\n"));
}
#[test]
fn encodes_validation_frame_with_full_shape() {
let frame = Frame::Validation {
ok: true,
warnings: vec![],
audit: audit_fixture(),
};
let out = encode(&frame);
assert!(out.starts_with("event: validation\n"));
assert!(out.contains("\"ok\":true"));
assert!(out.contains("\"prompt_tokens\":123"));
assert!(out.contains("\"cache_hit\":false"));
assert!(out.ends_with("\n\n"));
}
#[test]
fn validation_carries_warnings_array() {
let frame = Frame::Validation {
ok: false,
warnings: vec![
ValidationWarning {
kind: "out_of_range".to_string(),
detail: "[^9] but only 3 sources".to_string(),
},
ValidationWarning {
kind: "mode_fallback".to_string(),
detail: "ollama".to_string(),
},
],
audit: audit_fixture(),
};
let out = encode(&frame);
assert!(out.contains("\"kind\":\"out_of_range\""));
assert!(out.contains("\"kind\":\"mode_fallback\""));
assert!(out.contains("\"ok\":false"));
}
#[test]
fn encodes_error_frame_with_code() {
let frame = Frame::Error {
code: 413,
message: "max_prompt_tokens exceeded".to_string(),
};
let out = encode(&frame);
assert_eq!(
out,
"event: error\ndata: {\"code\":413,\"message\":\"max_prompt_tokens exceeded\"}\n\n"
);
}
#[test]
fn error_frame_handles_504_timeout() {
let frame = Frame::Error {
code: 504,
message: "timeout_ms exceeded".to_string(),
};
let out = encode(&frame);
assert!(out.contains("\"code\":504"));
}
#[test]
fn multiline_payload_splits_across_data_lines() {
let frame = Frame::AnswerToken {
text: "line1\nline2".to_string(),
};
let out = encode(&frame);
assert_eq!(
out,
"event: answer_token\ndata: {\"text\":\"line1\\nline2\"}\n\n"
);
}
#[test]
fn encoder_splits_on_literal_newlines_in_payload() {
let mut out = String::new();
out.push_str("event: x\n");
for line in "a\nb\nc".split('\n') {
out.push_str("data: ");
out.push_str(line);
out.push('\n');
}
out.push('\n');
assert_eq!(out, "event: x\ndata: a\ndata: b\ndata: c\n\n");
}
#[test]
fn frame_terminator_is_double_newline() {
for frame in [
Frame::Sources {
sources_flat: vec![],
},
Frame::AnswerToken {
text: String::new(),
},
Frame::Validation {
ok: true,
warnings: vec![],
audit: audit_fixture(),
},
Frame::Error {
code: 500,
message: String::new(),
},
] {
let out = encode(&frame);
assert!(out.ends_with("\n\n"), "frame missing terminator: {:?}", out);
assert!(!out.ends_with("\n\n\n"));
}
}
#[test]
fn sources_frame_with_empty_list_is_well_formed() {
let frame = Frame::Sources {
sources_flat: vec![],
};
let out = encode(&frame);
assert_eq!(out, "event: sources\ndata: {\"sources_flat\":[]}\n\n");
}
#[test]
fn answer_token_with_empty_text_is_well_formed() {
let frame = Frame::AnswerToken {
text: String::new(),
};
let out = encode(&frame);
assert_eq!(out, "event: answer_token\ndata: {\"text\":\"\"}\n\n");
}
#[test]
fn encoding_is_deterministic_across_calls() {
let frame = Frame::Validation {
ok: true,
warnings: vec![ValidationWarning {
kind: "k".to_string(),
detail: "d".to_string(),
}],
audit: audit_fixture(),
};
let a = encode(&frame);
let b = encode(&frame);
assert_eq!(a, b);
}
#[test]
fn event_name_matches_pinned_constants() {
assert_eq!(
Frame::Sources {
sources_flat: vec![]
}
.event_name(),
event::SOURCES
);
assert_eq!(
Frame::AnswerToken {
text: String::new()
}
.event_name(),
event::ANSWER_TOKEN
);
assert_eq!(
Frame::Validation {
ok: true,
warnings: vec![],
audit: audit_fixture(),
}
.event_name(),
event::VALIDATION
);
assert_eq!(
Frame::Error {
code: 0,
message: String::new(),
}
.event_name(),
event::ERROR
);
}
#[test]
fn unicode_in_token_text_passes_through() {
let frame = Frame::AnswerToken {
text: "olá 🌍".to_string(),
};
let out = encode(&frame);
assert!(out.contains("olá 🌍"));
assert!(out.ends_with("\n\n"));
}
}