use rmcp::model::{ContentBlock, ResourceContents};
use zeph_common::text::{truncate_to_bytes, truncate_to_bytes_ref};
const MAX_BLOCK_RENDER_BYTES: usize = 8192;
const MAX_RENDERED_BLOCKS: usize = 64;
#[must_use]
pub fn render_content_block(block: &ContentBlock) -> String {
let rendered = match block {
ContentBlock::Text(t) => truncate_to_bytes_ref(&t.text, MAX_BLOCK_RENDER_BYTES).to_owned(),
ContentBlock::Image(img) => {
format!("[image: {}, {} bytes]", img.mime_type, img.data.len())
}
ContentBlock::Audio(audio) => {
format!("[audio: {}, {} bytes]", audio.mime_type, audio.data.len())
}
ContentBlock::Resource(res) => render_resource_contents(&res.resource),
ContentBlock::ResourceLink(link) => {
format!("[resource_link: {} ({})]", link.uri, link.name)
}
_ => "[unsupported content block]".to_owned(),
};
truncate_to_bytes(&rendered, MAX_BLOCK_RENDER_BYTES)
}
fn render_resource_contents(resource: &ResourceContents) -> String {
match resource {
ResourceContents::TextResourceContents { uri, text, .. } => {
let text = truncate_to_bytes_ref(text, MAX_BLOCK_RENDER_BYTES);
format!("[resource: {uri}]\n{text}")
}
ResourceContents::BlobResourceContents {
uri,
mime_type,
blob,
..
} => {
let mime = mime_type.as_deref().unwrap_or("application/octet-stream");
format!("[resource: {uri} {mime} {} bytes]", blob.len())
}
_ => "[unsupported resource content]".to_owned(),
}
}
#[must_use]
pub fn render_content_blocks(content: &[ContentBlock]) -> String {
if content.is_empty() {
return String::new();
}
let total = content.len();
let rendered_count = total.min(MAX_RENDERED_BLOCKS);
let mut parts: Vec<String> = content[..rendered_count]
.iter()
.map(render_content_block)
.collect();
if total > MAX_RENDERED_BLOCKS {
parts.push(format!(
"[{} more content block(s) omitted]",
total - MAX_RENDERED_BLOCKS
));
}
parts.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::Resource;
#[test]
fn renders_empty_slice_as_empty_string() {
assert_eq!(render_content_blocks(&[]), "");
}
#[test]
fn renders_text_block_as_is() {
let block = ContentBlock::text("hello world");
assert_eq!(render_content_block(&block), "hello world");
}
#[test]
fn joins_mixed_blocks_with_newline() {
let blocks = vec![
ContentBlock::text("first"),
ContentBlock::image("ZGF0YQ==", "image/png"),
];
let rendered = render_content_blocks(&blocks);
assert_eq!(rendered, "first\n[image: image/png, 8 bytes]");
}
#[test]
fn renders_image_placeholder_without_payload() {
let block = ContentBlock::image("c2VjcmV0Ynl0ZXM=", "image/jpeg");
let rendered = render_content_block(&block);
assert!(rendered.starts_with("[image: image/jpeg,"));
assert!(rendered.ends_with("bytes]"));
assert!(!rendered.contains("c2VjcmV0Ynl0ZXM="));
}
#[test]
fn renders_audio_placeholder_without_payload() {
let block = ContentBlock::audio("c2VjcmV0YXVkaW8=", "audio/wav");
let rendered = render_content_block(&block);
assert!(rendered.starts_with("[audio: audio/wav,"));
assert!(!rendered.contains("c2VjcmV0YXVkaW8="));
}
#[test]
fn renders_embedded_text_resource_with_uri_and_text() {
let block = ContentBlock::embedded_text("file:///notes.txt", "important resource content");
let rendered = render_content_block(&block);
assert!(rendered.contains("file:///notes.txt"));
assert!(rendered.contains("important resource content"));
}
#[test]
fn renders_blob_resource_as_byte_count_placeholder() {
let block = ContentBlock::resource(ResourceContents::blob("Ymxvb2I=", "file:///x.bin"));
let rendered = render_content_block(&block);
assert!(rendered.starts_with("[resource: file:///x.bin"));
assert!(rendered.ends_with("bytes]"));
assert!(!rendered.contains("Ymxvb2I="));
}
#[test]
fn renders_resource_link_with_uri_and_name() {
let resource = Resource::new("file:///report.pdf", "report.pdf");
let block = ContentBlock::resource_link(resource);
let rendered = render_content_block(&block);
assert_eq!(rendered, "[resource_link: file:///report.pdf (report.pdf)]");
}
#[test]
fn truncates_oversized_text_block() {
let huge = "a".repeat(MAX_BLOCK_RENDER_BYTES * 2);
let block = ContentBlock::text(huge);
let rendered = render_content_block(&block);
assert_eq!(rendered.len(), MAX_BLOCK_RENDER_BYTES);
}
#[test]
fn text_block_at_exactly_cap_is_not_truncated() {
let exact = "a".repeat(MAX_BLOCK_RENDER_BYTES);
let block = ContentBlock::text(exact.clone());
let rendered = render_content_block(&block);
assert_eq!(rendered.len(), MAX_BLOCK_RENDER_BYTES);
assert_eq!(rendered, exact);
}
#[test]
fn text_block_one_byte_over_cap_truncates_to_exactly_cap() {
let over = "a".repeat(MAX_BLOCK_RENDER_BYTES + 1);
let block = ContentBlock::text(over);
let rendered = render_content_block(&block);
assert_eq!(rendered.len(), MAX_BLOCK_RENDER_BYTES);
}
#[test]
fn caps_total_blocks_rendered() {
let blocks: Vec<ContentBlock> = (0..(MAX_RENDERED_BLOCKS + 10))
.map(|i| ContentBlock::text(format!("block-{i}")))
.collect();
let rendered = render_content_blocks(&blocks);
assert!(rendered.contains("10 more content block(s) omitted"));
assert_eq!(rendered.lines().count(), MAX_RENDERED_BLOCKS + 1);
}
#[test]
fn exactly_max_rendered_blocks_has_no_omitted_marker() {
let blocks: Vec<ContentBlock> = (0..MAX_RENDERED_BLOCKS)
.map(|i| ContentBlock::text(format!("block-{i}")))
.collect();
let rendered = render_content_blocks(&blocks);
assert!(!rendered.contains("omitted"));
assert_eq!(rendered.lines().count(), MAX_RENDERED_BLOCKS);
}
#[test]
fn one_block_over_cap_renders_cap_blocks_plus_single_omitted_marker() {
let blocks: Vec<ContentBlock> = (0..=MAX_RENDERED_BLOCKS)
.map(|i| ContentBlock::text(format!("block-{i}")))
.collect();
let rendered = render_content_blocks(&blocks);
assert!(rendered.contains("1 more content block(s) omitted"));
assert_eq!(rendered.lines().count(), MAX_RENDERED_BLOCKS + 1);
}
}