use crate::value::VmValue;
pub(crate) trait MessageView {
fn has_field(&self, key: &str) -> bool;
fn str_field(&self, key: &str) -> Option<&str>;
fn bool_field(&self, key: &str) -> Option<bool>;
fn str_list_field(&self, key: &str) -> Option<Vec<&str>>;
fn list_len_field(&self, key: &str) -> Option<usize>;
fn content_text(&self) -> String;
}
impl MessageView for serde_json::Value {
fn has_field(&self, key: &str) -> bool {
self.get(key).is_some()
}
fn str_field(&self, key: &str) -> Option<&str> {
self.get(key).and_then(serde_json::Value::as_str)
}
fn bool_field(&self, key: &str) -> Option<bool> {
self.get(key).and_then(serde_json::Value::as_bool)
}
fn str_list_field(&self, key: &str) -> Option<Vec<&str>> {
self.get(key)
.and_then(serde_json::Value::as_array)
.map(|items| items.iter().filter_map(serde_json::Value::as_str).collect())
}
fn list_len_field(&self, key: &str) -> Option<usize> {
self.get(key)
.and_then(serde_json::Value::as_array)
.map(Vec::len)
}
fn content_text(&self) -> String {
match self.get("content") {
Some(serde_json::Value::String(text)) => text.clone(),
Some(serde_json::Value::Array(blocks)) => blocks
.iter()
.filter_map(|block| block.str_field("text"))
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
}
impl MessageView for VmValue {
fn has_field(&self, key: &str) -> bool {
self.as_dict().is_some_and(|dict| dict.contains_key(key))
}
fn str_field(&self, key: &str) -> Option<&str> {
match self.as_dict()?.get(key)? {
VmValue::String(text) => Some(text.as_str()),
_ => None,
}
}
fn bool_field(&self, key: &str) -> Option<bool> {
match self.as_dict()?.get(key)? {
VmValue::Bool(value) => Some(*value),
_ => None,
}
}
fn str_list_field(&self, key: &str) -> Option<Vec<&str>> {
match self.as_dict()?.get(key)? {
VmValue::List(items) => Some(
items
.iter()
.filter_map(|item| match item {
VmValue::String(text) => Some(text.as_str()),
_ => None,
})
.collect(),
),
_ => None,
}
}
fn list_len_field(&self, key: &str) -> Option<usize> {
match self.as_dict()?.get(key)? {
VmValue::List(items) => Some(items.len()),
_ => None,
}
}
fn content_text(&self) -> String {
match self.as_dict().and_then(|dict| dict.get("content")) {
Some(VmValue::String(text)) => text.to_string(),
Some(VmValue::List(blocks)) => blocks
.iter()
.filter_map(|block| block.str_field("text"))
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
}
pub(crate) fn displays_as(value: &VmValue, expected: &str) -> bool {
match value {
VmValue::String(text) => text.as_str() == expected,
other => other.display() == expected,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vm_and_json_views_agree() {
let samples = [
serde_json::json!({"role": "user", "content": "plain"}),
serde_json::json!({
"role": "assistant",
"content": [{"type": "text", "text": "a"}, {"type": "image"}, {"text": "b"}],
"tool_calls": [{"id": "1"}],
"_harn_directive_ids": ["r1", 7, "r2"],
"_bookkeeping": true,
"nil_field": null,
}),
serde_json::json!({"role": 3, "content": {"text": "not a list"}, "tool_calls": "x"}),
serde_json::json!("not an object"),
];
let keys = [
"role",
"content",
"tool_calls",
"_harn_directive_ids",
"_bookkeeping",
"nil_field",
"missing",
];
for json in samples {
let vm = crate::schema::json_to_vm_value(&json);
assert_eq!(json.content_text(), vm.content_text(), "{json}");
for key in keys {
assert_eq!(json.has_field(key), vm.has_field(key), "{json} {key}");
assert_eq!(json.str_field(key), vm.str_field(key), "{json} {key}");
assert_eq!(json.bool_field(key), vm.bool_field(key), "{json} {key}");
assert_eq!(
json.str_list_field(key),
vm.str_list_field(key),
"{json} {key}"
);
assert_eq!(
json.list_len_field(key),
vm.list_len_field(key),
"{json} {key}"
);
}
}
}
}