use stream_rs::accumulators::gemini::GeminiAccumulator;
use stream_rs::incremental_json::JsonSplitter;
fn wire_chunks() -> Vec<&'static [u8]> {
vec![
br#"{"candidates":[{"content":{"parts":[{"text":"The weath"#,
br#"er"}]},"index":0}]}"#,
br#"{"candidates":[{"content":{"parts":[{"text":" in Paris"}]},"index":0}]}"#,
br#"{"candidates":[{"content":{"parts":[{"functionCall":{"name":"get_weather","args":{"city":"Paris"}}}]},"index":0}]}"#,
br#"{"candidates":[{"finishReason":"STOP","index":0}]}"#,
]
}
fn main() {
let mut splitter = JsonSplitter::new();
let mut acc = GeminiAccumulator::new();
let mut values = Vec::new();
for chunk in wire_chunks() {
values.clear();
splitter.feed(chunk, &mut values);
for value in &values {
apply_value(value, &mut acc);
}
}
splitter
.finish(&mut values)
.expect("Gemini stream ended on a clean JSON boundary");
let c = acc.candidate(0).expect("a candidate was streamed");
println!("text : {:?}", c.text);
println!("finish_reason : {:?}", c.finish_reason.as_deref());
for (i, call) in c.function_calls.iter().enumerate() {
println!(
"function_call[{i}]: name={:?} args={}",
call.name, call.args
);
}
assert_eq!(c.text, "The weather in Paris");
assert_eq!(c.finish_reason.as_deref(), Some("STOP"));
assert_eq!(c.function_calls[0].name, "get_weather");
assert_eq!(c.function_calls[0].args, r#"{"city":"Paris"}"#);
println!("\nOK: Gemini stream reassembled correctly across ragged chunk boundaries.");
}
fn apply_value(json: &str, acc: &mut GeminiAccumulator) {
let index = 0;
if let Some(text) = string_after(json, "\"text\":\"") {
acc.push_text(index, &unescape(&text));
}
if let Some(name) = string_after(json, "\"name\":\"") {
if let Some(args) = object_after(json, "\"args\":") {
acc.push_function_call(index, &name, &args);
}
}
if let Some(reason) = string_after(json, "\"finishReason\":\"") {
acc.set_finish_reason(index, &reason);
}
}
fn string_after(haystack: &str, marker: &str) -> Option<String> {
let start = haystack.find(marker)? + marker.len();
let bytes = haystack.as_bytes();
let mut i = start;
let mut out = String::new();
while i < bytes.len() {
match bytes[i] {
b'\\' if i + 1 < bytes.len() => {
out.push('\\');
out.push(bytes[i + 1] as char);
i += 2;
}
b'"' => return Some(out),
b => {
out.push(b as char);
i += 1;
}
}
}
Some(out)
}
fn object_after(haystack: &str, marker: &str) -> Option<String> {
let start = haystack.find(marker)? + marker.len();
let bytes = haystack.as_bytes();
if bytes.get(start) != Some(&b'{') {
return None;
}
let mut depth = 0usize;
let mut in_string = false;
let mut escaped = false;
let mut out = String::new();
for &b in &bytes[start..] {
out.push(b as char);
if in_string {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
in_string = false;
}
continue;
}
match b {
b'"' => in_string = true,
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return Some(out);
}
}
_ => {}
}
}
None
}
fn unescape(s: &str) -> String {
s.replace("\\\"", "\"").replace("\\\\", "\\")
}