1pub const STREAMING_VIRTUAL_FIELDS: &[&str] = &[
34 "chunks",
35 "chunks.length",
36 "stream_content",
37 "stream_complete",
38 "no_chunks_after_done",
39 "tool_calls",
40 "finish_reason",
41 "stream.has_page_event",
46 "stream.has_error_event",
47 "stream.has_complete_event",
48 "stream.event_count_min",
49];
50
51const STREAMING_VIRTUAL_ROOTS: &[&str] = &["tool_calls", "finish_reason"];
61
62pub fn is_streaming_virtual_field(field: &str) -> bool {
70 if STREAMING_VIRTUAL_FIELDS.contains(&field) {
71 return true;
72 }
73 for root in STREAMING_VIRTUAL_ROOTS {
75 if field.len() > root.len() && field.starts_with(root) {
76 let rest = &field[root.len()..];
77 if rest.starts_with('[') || rest.starts_with('.') {
78 return true;
79 }
80 }
81 }
82 false
83}
84
85fn split_streaming_deep_path(field: &str) -> Option<(&str, &str)> {
91 for root in STREAMING_VIRTUAL_ROOTS {
92 if field.len() > root.len() && field.starts_with(root) {
93 let rest = &field[root.len()..];
94 if rest.starts_with('[') || rest.starts_with('.') {
95 return Some((root, rest));
96 }
97 }
98 }
99 None
100}
101
102const STREAMING_ONLY_AUTO_DETECT_FIELDS: &[&str] = &[
109 "chunks",
110 "chunks.length",
111 "stream_content",
112 "stream_complete",
113 "no_chunks_after_done",
114 "stream.has_page_event",
115 "stream.has_error_event",
116 "stream.has_complete_event",
117 "stream.event_count_min",
118];
119
120pub fn resolve_is_streaming(fixture: &crate::fixture::Fixture, call_streaming: Option<bool>) -> bool {
132 if let Some(forced) = call_streaming {
133 return forced;
134 }
135 fixture.is_streaming_mock()
136 || fixture.assertions.iter().any(|a| {
137 a.field
138 .as_deref()
139 .is_some_and(|f| !f.is_empty() && STREAMING_ONLY_AUTO_DETECT_FIELDS.contains(&f))
140 })
141}
142
143pub struct StreamingFieldResolver;
145
146impl StreamingFieldResolver {
147 pub fn accessor(field: &str, lang: &str, chunks_var: &str) -> Option<String> {
153 match field {
154 "chunks" => Some(match lang {
155 "zig" => format!("{chunks_var}.items"),
157 "php" => format!("${chunks_var}"),
160 _ => chunks_var.to_string(),
161 }),
162
163 "chunks.length" => Some(match lang {
164 "rust" => format!("{chunks_var}.len()"),
165 "go" => format!("len({chunks_var})"),
166 "python" => format!("len({chunks_var})"),
167 "php" => format!("count(${chunks_var})"),
168 "elixir" => format!("length({chunks_var})"),
169 "kotlin" => format!("{chunks_var}.size"),
171 "zig" => format!("{chunks_var}.items.len"),
173 "swift" => format!("{chunks_var}.count"),
175 _ => format!("{chunks_var}.length"),
177 }),
178
179 "stream_content" => Some(match lang {
180 "rust" => {
181 format!(
182 "{chunks_var}.iter().map(|c| c.choices.first().and_then(|ch| ch.delta.content.as_deref()).unwrap_or(\"\")).collect::<String>()"
183 )
184 }
185 "go" => {
186 format!(
188 "func() string {{ var s string; for _, c := range {chunks_var} {{ if len(c.Choices) > 0 && c.Choices[0].Delta.Content != nil {{ s += *c.Choices[0].Delta.Content }} }}; return s }}()"
189 )
190 }
191 "java" => {
192 format!(
193 "{chunks_var}.stream().map(c -> c.choices().stream().findFirst().map(ch -> ch.delta().content() != null ? ch.delta().content() : \"\").orElse(\"\")).collect(java.util.stream.Collectors.joining())"
194 )
195 }
196 "php" => {
197 format!("implode('', array_map(fn($c) => $c->choices[0]->delta->content ?? '', ${chunks_var}))")
198 }
199 "kotlin" => {
200 format!(
203 "{chunks_var}.joinToString(\"\") {{ it.choices()?.firstOrNull()?.delta()?.content() ?: \"\" }}"
204 )
205 }
206 "kotlin_android" => {
207 format!("{chunks_var}.joinToString(\"\") {{ it.choices?.firstOrNull()?.delta?.content ?: \"\" }}")
209 }
210 "elixir" => {
211 format!(
215 "{chunks_var} |> Enum.map(fn c -> (Enum.at(c.choices, 0) || %{{}}) |> Map.get(:delta, %{{}}) |> Map.get(:content, \"\") end) |> Enum.join(\"\")"
216 )
217 }
218 "python" => {
219 format!("\"\".join(c.choices[0].delta.content or \"\" for c in {chunks_var} if c.choices)")
220 }
221 "zig" => {
222 format!("{chunks_var}_content.items")
225 }
226 "swift" => {
230 format!(
231 "{chunks_var}.map {{ c in c.choices().first.flatMap {{ ch in ch.delta().content()?.toString() }} ?? \"\" }}.joined()"
232 )
233 }
234 _ => {
236 format!("{chunks_var}.map((c: any) => c.choices?.[0]?.delta?.content ?? '').join('')")
237 }
238 }),
239
240 "stream_complete" => Some(match lang {
241 "rust" => {
242 format!(
243 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).is_some()"
244 )
245 }
246 "go" => {
247 format!(
248 "func() bool {{ if len({chunks_var}) == 0 {{ return false }}; last := {chunks_var}[len({chunks_var})-1]; return len(last.Choices) > 0 && last.Choices[0].FinishReason != nil }}()"
249 )
250 }
251 "java" => {
252 format!(
253 "!{chunks_var}.isEmpty() && {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().flatMap(ch -> java.util.Optional.ofNullable(ch.finishReason())).isPresent()"
254 )
255 }
256 "php" => {
257 format!("!empty(${chunks_var}) && isset(end(${chunks_var})->choices[0]->finishReason)")
261 }
262 "kotlin" => {
263 format!(
265 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices()?.firstOrNull()?.finishReason() != null"
266 )
267 }
268 "kotlin_android" => {
269 format!(
271 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices?.firstOrNull()?.finishReason != null"
272 )
273 }
274 "python" => {
275 format!("bool({chunks_var}) and {chunks_var}[-1].choices[0].finish_reason is not None")
276 }
277 "elixir" => {
278 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason != nil")
279 }
280 "zig" => {
283 format!("{chunks_var}.items.len > 0")
284 }
285 "swift" => {
289 format!("!{chunks_var}.isEmpty && {chunks_var}.last!.choices().first?.finish_reason() != nil")
290 }
291 _ => {
293 format!(
294 "{chunks_var}.length > 0 && {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason != null"
295 )
296 }
297 }),
298
299 "no_chunks_after_done" => Some(match lang {
303 "rust" => "true".to_string(),
304 "go" => "true".to_string(),
305 "java" => "true".to_string(),
306 "php" => "true".to_string(),
307 _ => "true".to_string(),
308 }),
309
310 "stream.has_page_event" => has_event_variant_accessor(lang, chunks_var, EventVariant::Page),
320 "stream.has_error_event" => has_event_variant_accessor(lang, chunks_var, EventVariant::Error),
321 "stream.has_complete_event" => has_event_variant_accessor(lang, chunks_var, EventVariant::Complete),
322
323 "stream.event_count_min" => Self::accessor("chunks.length", lang, chunks_var),
327
328 "tool_calls" => Some(match lang {
329 "rust" => {
330 format!(
331 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten())).collect::<Vec<_>>()"
332 )
333 }
334 "go" => {
335 format!(
339 "func() []pkg.StreamToolCall {{ var tc []pkg.StreamToolCall; for _, c := range {chunks_var} {{ for _, ch := range c.Choices {{ tc = append(tc, ch.Delta.ToolCalls...) }} }}; return tc }}()"
340 )
341 }
342 "java" => {
343 format!(
344 "{chunks_var}.stream().flatMap(c -> c.choices().stream()).flatMap(ch -> ch.delta().toolCalls() != null ? ch.delta().toolCalls().stream() : java.util.stream.Stream.empty()).toList()"
345 )
346 }
347 "php" => {
348 format!(
351 "array_merge(...array_map(fn($c) => $c->choices[0]->delta->toolCalls ?? [], ${chunks_var}))"
352 )
353 }
354 "kotlin" => {
355 format!(
357 "{chunks_var}.flatMap {{ c -> c.choices()?.flatMap {{ ch -> ch.delta()?.toolCalls() ?: emptyList() }} ?: emptyList() }}"
358 )
359 }
360 "kotlin_android" => {
361 format!(
363 "{chunks_var}.flatMap {{ c -> c.choices?.flatMap {{ ch -> ch.delta?.toolCalls ?: emptyList() }} ?: emptyList() }}"
364 )
365 }
366 "python" => {
367 format!(
368 "[t for c in {chunks_var} for ch in (c.choices or []) for t in (ch.delta.tool_calls or [])]"
369 )
370 }
371 "elixir" => {
372 format!(
373 "{chunks_var} |> Enum.flat_map(fn c -> (List.first(c.choices) || %{{}}).delta |> Map.get(:tool_calls, []) end)"
374 )
375 }
376 "zig" => {
378 format!("{chunks_var}.items")
379 }
380 "swift" => {
387 format!(
388 "{chunks_var}.flatMap {{ c -> [StreamToolCallRef] in guard let ch = c.choices().first, let tcs = ch.delta().tool_calls() else {{ return [] }}; return Array(tcs) }}"
389 )
390 }
391 _ => {
392 format!("{chunks_var}.flatMap((c: any) => c.choices?.[0]?.delta?.toolCalls ?? [])")
393 }
394 }),
395
396 "finish_reason" => Some(match lang {
397 "rust" => {
398 format!(
401 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).map(|v| v.to_string()).unwrap_or_default()"
402 )
403 }
404 "go" => {
405 format!(
408 "func() string {{ if len({chunks_var}) == 0 {{ return \"\" }}; last := {chunks_var}[len({chunks_var})-1]; if len(last.Choices) > 0 && last.Choices[0].FinishReason != nil {{ return string(*last.Choices[0].FinishReason) }}; return \"\" }}()"
409 )
410 }
411 "java" => {
412 format!(
416 "({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().map(ch -> ch.finishReason() == null ? null : ch.finishReason().getValue()).orElse(null))"
417 )
418 }
419 "php" => {
420 format!("(!empty(${chunks_var}) ? (end(${chunks_var})->choices[0]->finishReason ?? null) : null)")
423 }
424 "kotlin" => {
425 format!(
428 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices()?.firstOrNull()?.finishReason()?.getValue())"
429 )
430 }
431 "kotlin_android" => {
432 format!(
434 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices?.firstOrNull()?.finishReason?.name?.lowercase())"
435 )
436 }
437 "python" => {
438 format!(
442 "(str({chunks_var}[-1].choices[0].finish_reason) if {chunks_var} and {chunks_var}[-1].choices else None)"
443 )
444 }
445 "elixir" => {
446 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason")
447 }
448 "zig" => {
451 format!(
452 "(blk: {{ if ({chunks_var}.items.len == 0) break :blk \"\"; var _lcp = std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {chunks_var}.items[{chunks_var}.items.len - 1], .{{}}) catch break :blk \"\"; defer _lcp.deinit(); if (_lcp.value.object.get(\"choices\")) |_lchs| if (_lchs.array.items.len > 0) if (_lchs.array.items[0].object.get(\"finish_reason\")) |_fr| if (_fr == .string) break :blk _fr.string; break :blk \"\"; }})"
453 )
454 }
455 "swift" => {
459 format!(
460 "({chunks_var}.isEmpty ? nil : {chunks_var}.last!.choices().first?.finish_reason()?.toString())"
461 )
462 }
463 _ => {
464 format!(
465 "{chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason : undefined"
466 )
467 }
468 }),
469
470 "usage" => Some(match lang {
475 "python" => {
476 format!("({chunks_var}[-1].usage if {chunks_var} else None)")
480 }
481 "rust" => {
482 format!("{chunks_var}.last().and_then(|c| c.usage.as_ref())")
483 }
484 "go" => {
485 format!(
486 "func() interface{{}} {{ if len({chunks_var}) == 0 {{ return nil }}; return {chunks_var}[len({chunks_var})-1].Usage }}()"
487 )
488 }
489 "java" => {
490 format!("({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).usage())")
491 }
492 "kotlin" => {
493 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage())")
494 }
495 "kotlin_android" => {
496 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage)")
498 }
499 "php" => {
500 format!("(!empty(${chunks_var}) ? end(${chunks_var})->usage ?? null : null)")
501 }
502 "elixir" => {
503 format!("(if length({chunks_var}) > 0, do: List.last({chunks_var}).usage, else: nil)")
504 }
505 "swift" => {
507 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.usage())")
508 }
509 _ => {
510 format!("({chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].usage : undefined)")
511 }
512 }),
513
514 _ => {
515 if let Some((root, tail)) = split_streaming_deep_path(field) {
519 if lang == "rust" && root == "tool_calls" {
523 return Some(render_rust_tool_calls_deep(chunks_var, tail));
524 }
525 if lang == "swift" && root == "tool_calls" {
529 let root_expr = Self::accessor(root, lang, chunks_var)?;
530 return Some(render_swift_tool_calls_deep(&root_expr, tail));
531 }
532 if lang == "zig" && root == "tool_calls" {
539 return None;
540 }
541 let root_expr = Self::accessor(root, lang, chunks_var)?;
542 Some(render_deep_tail(&root_expr, tail, lang))
543 } else {
544 None
545 }
546 }
547 }
548 }
549
550 pub fn collect_snippet(lang: &str, stream_var: &str, chunks_var: &str) -> Option<String> {
556 match lang {
557 "rust" => Some(format!(
558 "let {chunks_var}: Vec<_> = tokio_stream::StreamExt::collect::<Vec<_>>({stream_var}).await\n .into_iter()\n .map(|r| r.expect(\"stream item failed\"))\n .collect();"
559 )),
560 "go" => Some(format!(
561 "var {chunks_var} []pkg.ChatCompletionChunk\n\tfor chunk := range {stream_var} {{\n\t\t{chunks_var} = append({chunks_var}, chunk)\n\t}}"
562 )),
563 "java" => Some(format!(
564 "var {chunks_var} = new java.util.ArrayList<ChatCompletionChunk>();\n var _it = {stream_var}.iterator();\n while (_it.hasNext()) {{ {chunks_var}.add(_it.next()); }}"
565 )),
566 "php" => Some(format!(
576 "$__camel = function ($v) use (&$__camel) {{ \
577 if (is_array($v)) {{ \
578 $out = []; \
579 foreach ($v as $k => $vv) {{ \
580 $key = is_string($k) ? lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))) : $k; \
581 $out[$key] = $__camel($vv); \
582 }} \
583 return (array_keys($out) === range(0, count($out) - 1)) ? $out : (object) $out; \
584 }} \
585 if (is_object($v)) {{ \
586 $out = new \\stdClass(); \
587 foreach (get_object_vars($v) as $k => $vv) {{ \
588 $key = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))); \
589 $out->{{$key}} = $__camel($vv); \
590 }} \
591 return $out; \
592 }} \
593 return $v; \
594 }};\n \
595 $__decode_chunk = fn($c) => $__camel(is_string($c) ? json_decode($c, true) : (is_array($c) || is_object($c) ? json_decode(json_encode($c), true) : $c));\n \
596 ${chunks_var} = is_string(${stream_var}) \
597 ? array_map($__decode_chunk, (array)(json_decode(${stream_var}, true) ?: [])) \
598 : (is_array(${stream_var}) \
599 ? array_map($__decode_chunk, ${stream_var}) \
600 : array_map($__decode_chunk, iterator_to_array(${stream_var})));"
601 )),
602 "python" => Some(format!(
603 "{chunks_var} = []\n async for chunk in {stream_var}:\n {chunks_var}.append(chunk)"
604 )),
605 "kotlin" => {
606 Some(format!("val {chunks_var} = {stream_var}.asSequence().toList()"))
609 }
610 "kotlin_android" => {
611 Some(format!("val {chunks_var} = {stream_var}.toList()"))
614 }
615 "elixir" => Some(format!("{chunks_var} = Enum.to_list({stream_var})")),
616 "wasm" => Some(format!(
623 "const {chunks_var}: any[] = [];\n while (true) {{ const _chunk = await {stream_var}.next(); if (_chunk == null) break; {chunks_var}.push(_chunk); }}"
624 )),
625 "node" | "typescript" => Some(format!(
626 "const {chunks_var}: any[] = [];\n for await (const _chunk of {stream_var}) {{ {chunks_var}.push(_chunk); }}"
627 )),
628 "swift" => {
629 Some(format!(
634 "var {chunks_var}: [ChatCompletionChunk] = []\n for try await _chunk in {stream_var} {{ {chunks_var}.append(_chunk) }}"
635 ))
636 }
637 "zig" => Some(Self::collect_snippet_zig(stream_var, chunks_var, "module", "ffi")),
638 _ => None,
639 }
640 }
641
642 pub fn collect_snippet_zig(stream_var: &str, chunks_var: &str, module_name: &str, ffi_prefix: &str) -> String {
644 let stream_next = format!("{ffi_prefix}_default_client_chat_stream_next");
645 let chunk_to_json = format!("{ffi_prefix}_chat_completion_chunk_to_json");
646 let chunk_free = format!("{ffi_prefix}_chat_completion_chunk_free");
647 let free_string = format!("{ffi_prefix}_free_string");
648
649 format!(
656 concat!(
657 "var {chunks_var}: std.ArrayList([]u8) = .empty;
658",
659 " defer {{
660",
661 " for ({chunks_var}.items) |_cj| std.heap.c_allocator.free(_cj);
662",
663 " {chunks_var}.deinit(std.heap.c_allocator);
664",
665 " }}
666",
667 " var {chunks_var}_content: std.ArrayList(u8) = .empty;
668",
669 " defer {chunks_var}_content.deinit(std.heap.c_allocator);
670",
671 " while (true) {{
672",
673 " const _nc = {module_name}.c.{stream_next}({stream_var});
674",
675 " if (_nc == null) break;
676",
677 " const _np = {module_name}.c.{chunk_to_json}(_nc);
678",
679 " {module_name}.c.{chunk_free}(_nc);
680",
681 " if (_np == null) continue;
682",
683 " const _ns = std.mem.span(_np);
684",
685 " const _nj = try std.heap.c_allocator.dupe(u8, _ns);
686",
687 " {module_name}.c.{free_string}(_np);
688",
689 " if (std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, _nj, .{{}})) |_cp| {{
690",
691 " defer _cp.deinit();
692",
693 " if (_cp.value.object.get(\"choices\")) |_chs|
694",
695 " if (_chs.array.items.len > 0)
696",
697 " if (_chs.array.items[0].object.get(\"delta\")) |_dl|
698",
699 " if (_dl.object.get(\"content\")) |_ct|
700",
701 " if (_ct == .string) try {chunks_var}_content.appendSlice(std.heap.c_allocator, _ct.string);
702",
703 " }} else |_| {{}}
704",
705 " try {chunks_var}.append(std.heap.c_allocator, _nj);
706",
707 " }}"
708 ),
709 chunks_var = chunks_var,
710 stream_var = stream_var,
711 module_name = module_name,
712 stream_next = stream_next,
713 chunk_to_json = chunk_to_json,
714 chunk_free = chunk_free,
715 free_string = free_string,
716 )
717 }
718}
719
720#[derive(Debug, Clone, Copy)]
722enum EventVariant {
723 Page,
724 Error,
725 Complete,
726}
727
728impl EventVariant {
729 fn tag(self) -> &'static str {
731 match self {
732 EventVariant::Page => "page",
733 EventVariant::Error => "error",
734 EventVariant::Complete => "complete",
735 }
736 }
737
738 fn upper_camel(self) -> &'static str {
741 match self {
742 EventVariant::Page => "Page",
743 EventVariant::Error => "Error",
744 EventVariant::Complete => "Complete",
745 }
746 }
747}
748
749fn has_event_variant_accessor(lang: &str, chunks_var: &str, variant: EventVariant) -> Option<String> {
755 let tag = variant.tag();
756 let camel = variant.upper_camel();
757 match lang {
758 "python" => Some(format!("any(e.type == \"{tag}\" for e in {chunks_var})")),
760 "node" | "typescript" => Some(format!("{chunks_var}.some((e: any) => e?.type === \"{tag}\")")),
763 "ruby" => Some(format!("{chunks_var}.any? {{ |e| e.{tag}? }}")),
765 "go" => Some(format!(
769 "func() bool {{ for _, e := range {chunks_var} {{ if _, _ok := e.(pkg.CrawlEvent{camel}); _ok {{ return true }} }}; return false }}()"
770 )),
771 "java" => Some(format!(
773 "{chunks_var}.stream().anyMatch(e -> e instanceof CrawlEvent.{camel})"
774 )),
775 "csharp" => Some(format!(
777 "{chunks_var}.Any(e => e is global::Kreuzcrawl.CrawlEvent.{camel})"
778 )),
779 "swift" => Some(format!(
783 "{chunks_var}.contains(where: {{ e in if case .{tag} = e {{ return true }} else {{ return false }} }})"
784 )),
785 "elixir" => Some(format!(
787 "Enum.any?({chunks_var}, fn e -> Map.get(e, :type) == :{tag} end)"
788 )),
789 "kotlin" => Some(format!("{chunks_var}.any {{ it is CrawlEvent.{camel} }}")),
791 "kotlin_android" => Some(format!("{chunks_var}.any {{ it is CrawlEvent.{camel} }}")),
793 "dart" => Some(format!("{chunks_var}.any((e) => e is CrawlEvent_{camel})")),
796 "zig" => Some(format!(
801 "blk: {{ for ({chunks_var}.items) |_e| {{ if (std.mem.indexOf(u8, _e, \"\\\"type\\\":\\\"{tag}\\\"\") != null) break :blk true; }} break :blk false; }}"
802 )),
803 "rust" => Some(format!(
808 "{chunks_var}.iter().any(|e| matches!(e, kreuzcrawl::CrawlEvent::{camel} {{ .. }}))"
809 )),
810 "php" | "wasm" => None,
814 _ => None,
815 }
816}
817
818fn render_swift_tool_calls_deep(root_expr: &str, tail: &str) -> String {
830 use heck::ToLowerCamelCase;
831 let segs = parse_tail(tail);
832 let mut expr = root_expr.to_string();
833 let last_field_idx = segs.iter().rposition(|s| matches!(s, TailSeg::Field(_)));
834 let mut prev_was_index = false;
837
838 for (i, seg) in segs.iter().enumerate() {
839 match seg {
840 TailSeg::Index(n) => {
841 expr = format!("({expr})[{n}]");
842 prev_was_index = true;
843 }
844 TailSeg::Field(f) => {
845 let method = f.to_lower_camel_case();
846 let is_last = Some(i) == last_field_idx;
847 let chain = if prev_was_index { "." } else { "?." };
848 if is_last {
849 expr = format!("{expr}{chain}{method}()?.toString()");
851 } else {
852 expr = format!("{expr}{chain}{method}()");
854 }
855 prev_was_index = false;
856 }
857 }
858 }
859 expr
860}
861
862fn render_rust_tool_calls_deep(chunks_var: &str, tail: &str) -> String {
866 let segs = parse_tail(tail);
867 let idx = segs.iter().find_map(|s| match s {
869 TailSeg::Index(n) => Some(*n),
870 _ => None,
871 });
872 let field_segs: Vec<&str> = segs
873 .iter()
874 .filter_map(|s| match s {
875 TailSeg::Field(f) => Some(f.as_str()),
876 _ => None,
877 })
878 .collect();
879
880 let base = format!(
881 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten()))"
882 );
883 let with_nth = match idx {
884 Some(n) => format!("{base}.nth({n})"),
885 None => base,
886 };
887
888 let mut expr = with_nth;
891 for (i, f) in field_segs.iter().enumerate() {
892 let is_leaf = i == field_segs.len() - 1;
893 if is_leaf {
894 expr = format!("{expr}.and_then(|x| x.{f}.as_deref())");
895 } else {
896 expr = format!("{expr}.and_then(|x| x.{f}.as_ref())");
897 }
898 }
899 format!("{expr}.unwrap_or(\"\")")
900}
901
902#[derive(Debug, PartialEq)]
907enum TailSeg {
908 Index(usize),
909 Field(String),
910}
911
912fn parse_tail(tail: &str) -> Vec<TailSeg> {
913 let mut segs = Vec::new();
914 let mut rest = tail;
915 while !rest.is_empty() {
916 if let Some(inner) = rest.strip_prefix('[') {
917 if let Some(close) = inner.find(']') {
919 let idx_str = &inner[..close];
920 if let Ok(idx) = idx_str.parse::<usize>() {
921 segs.push(TailSeg::Index(idx));
922 }
923 rest = &inner[close + 1..];
924 } else {
925 break;
926 }
927 } else if let Some(inner) = rest.strip_prefix('.') {
928 let end = inner.find(['.', '[']).unwrap_or(inner.len());
930 segs.push(TailSeg::Field(inner[..end].to_string()));
931 rest = &inner[end..];
932 } else {
933 break;
934 }
935 }
936 segs
937}
938
939fn render_deep_tail(root_expr: &str, tail: &str, lang: &str) -> String {
942 use heck::{ToLowerCamelCase, ToPascalCase};
943
944 let segs = parse_tail(tail);
945 let mut out = root_expr.to_string();
946
947 for seg in &segs {
948 match (seg, lang) {
949 (TailSeg::Index(n), "rust") => {
950 out = format!("({out})[{n}]");
951 }
952 (TailSeg::Index(n), "java") => {
953 out = format!("({out}).get({n})");
954 }
955 (TailSeg::Index(n), "kotlin") => {
956 if *n == 0 {
957 out = format!("({out}).first()");
958 } else {
959 out = format!("({out}).get({n})");
960 }
961 }
962 (TailSeg::Index(n), "kotlin_android") => {
963 if *n == 0 {
964 out = format!("({out}).first()");
965 } else {
966 out = format!("({out})[{n}]");
967 }
968 }
969 (TailSeg::Index(n), "elixir") => {
970 out = format!("Enum.at({out}, {n})");
971 }
972 (TailSeg::Index(n), "zig") => {
973 out = format!("({out}).items[{n}]");
974 }
975 (TailSeg::Index(n), "php") => {
976 out = format!("({out})[{n}]");
977 }
978 (TailSeg::Index(n), _) => {
979 out = format!("({out})[{n}]");
981 }
982 (TailSeg::Field(f), "rust") => {
983 use heck::ToSnakeCase;
984 out.push('.');
985 out.push_str(&f.to_snake_case());
986 }
987 (TailSeg::Field(f), "go") => {
988 use alef_codegen::naming::to_go_name;
989 out.push('.');
990 out.push_str(&to_go_name(f));
991 }
992 (TailSeg::Field(f), "java") => {
993 out.push('.');
994 out.push_str(&f.to_lower_camel_case());
995 out.push_str("()");
996 }
997 (TailSeg::Field(f), "kotlin") => {
998 out.push_str("?.");
1004 out.push_str(&f.to_lower_camel_case());
1005 out.push_str("()");
1006 }
1007 (TailSeg::Field(f), "kotlin_android") => {
1008 out.push_str("?.");
1010 out.push_str(&f.to_lower_camel_case());
1011 }
1012 (TailSeg::Field(f), "csharp") => {
1013 out.push('.');
1014 out.push_str(&f.to_pascal_case());
1015 }
1016 (TailSeg::Field(f), "php") => {
1017 out.push_str("->");
1022 out.push_str(f);
1023 }
1024 (TailSeg::Field(f), "elixir") => {
1025 out.push('.');
1026 out.push_str(f);
1027 }
1028 (TailSeg::Field(f), "zig") => {
1029 out.push('.');
1030 out.push_str(f);
1031 }
1032 (TailSeg::Field(f), "python") | (TailSeg::Field(f), "ruby") => {
1033 out.push('.');
1034 out.push_str(f);
1035 }
1036 (TailSeg::Field(f), _) => {
1038 out.push('.');
1039 out.push_str(&f.to_lower_camel_case());
1040 }
1041 }
1042 }
1043
1044 out
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050
1051 #[test]
1052 fn is_streaming_virtual_field_recognizes_all_fields() {
1053 for field in STREAMING_VIRTUAL_FIELDS {
1054 assert!(
1055 is_streaming_virtual_field(field),
1056 "field '{field}' not recognized as streaming virtual"
1057 );
1058 }
1059 }
1060
1061 #[test]
1062 fn is_streaming_virtual_field_rejects_real_fields() {
1063 assert!(!is_streaming_virtual_field("content"));
1064 assert!(!is_streaming_virtual_field("choices"));
1065 assert!(!is_streaming_virtual_field("model"));
1066 assert!(!is_streaming_virtual_field(""));
1067 }
1068
1069 #[test]
1070 fn is_streaming_virtual_field_rejects_non_root_paths_with_matching_tail() {
1071 assert!(!is_streaming_virtual_field("choices[0].finish_reason"));
1076 assert!(!is_streaming_virtual_field("choices[0].message.content"));
1077 assert!(!is_streaming_virtual_field("data[0].embedding"));
1078 }
1079
1080 #[test]
1081 fn is_streaming_virtual_field_does_not_match_usage() {
1082 assert!(!is_streaming_virtual_field("usage"));
1086 assert!(!is_streaming_virtual_field("usage.total_tokens"));
1087 assert!(!is_streaming_virtual_field("usage.prompt_tokens"));
1088 }
1089
1090 #[test]
1091 fn accessor_chunks_returns_var_name() {
1092 assert_eq!(
1093 StreamingFieldResolver::accessor("chunks", "rust", "chunks"),
1094 Some("chunks".to_string())
1095 );
1096 assert_eq!(
1097 StreamingFieldResolver::accessor("chunks", "node", "chunks"),
1098 Some("chunks".to_string())
1099 );
1100 }
1101
1102 #[test]
1103 fn accessor_chunks_length_uses_language_idiom() {
1104 let rust = StreamingFieldResolver::accessor("chunks.length", "rust", "chunks").unwrap();
1105 assert!(rust.contains(".len()"), "rust: {rust}");
1106
1107 let go = StreamingFieldResolver::accessor("chunks.length", "go", "chunks").unwrap();
1108 assert!(go.starts_with("len("), "go: {go}");
1109
1110 let node = StreamingFieldResolver::accessor("chunks.length", "node", "chunks").unwrap();
1111 assert!(node.contains(".length"), "node: {node}");
1112
1113 let php = StreamingFieldResolver::accessor("chunks.length", "php", "chunks").unwrap();
1114 assert!(php.starts_with("count("), "php: {php}");
1115 }
1116
1117 #[test]
1118 fn accessor_chunks_length_zig_uses_items_len() {
1119 let zig = StreamingFieldResolver::accessor("chunks.length", "zig", "chunks").unwrap();
1120 assert_eq!(zig, "chunks.items.len", "zig chunks.length: {zig}");
1121 }
1122
1123 #[test]
1124 fn accessor_stream_content_zig_uses_content_items() {
1125 let zig = StreamingFieldResolver::accessor("stream_content", "zig", "chunks").unwrap();
1126 assert_eq!(zig, "chunks_content.items", "zig stream_content: {zig}");
1127 }
1128
1129 #[test]
1130 fn collect_snippet_zig_drains_via_ffi() {
1131 let snip = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks").unwrap();
1132 assert!(snip.contains("std.ArrayList([]u8)"), "zig collect: {snip}");
1133 assert!(snip.contains("chat_stream_next(_stream_handle)"), "zig collect: {snip}");
1134 assert!(snip.contains("chunks_content"), "zig collect: {snip}");
1135 assert!(
1136 snip.contains("chunks.append(std.heap.c_allocator"),
1137 "zig collect: {snip}"
1138 );
1139 assert!(snip.contains(".empty;"), "zig collect (Zig 0.16 unmanaged): {snip}");
1140 }
1141
1142 #[test]
1143 fn accessor_stream_content_rust_uses_iterator() {
1144 let expr = StreamingFieldResolver::accessor("stream_content", "rust", "chunks").unwrap();
1145 assert!(expr.contains(".collect::<String>()"), "rust stream_content: {expr}");
1146 }
1147
1148 #[test]
1149 fn accessor_no_chunks_after_done_returns_true() {
1150 for lang in ["rust", "go", "java", "php", "node", "wasm", "elixir"] {
1151 let expr = StreamingFieldResolver::accessor("no_chunks_after_done", lang, "chunks").unwrap();
1152 assert_eq!(expr, "true", "lang {lang}: expected 'true', got '{expr}'");
1153 }
1154 }
1155
1156 #[test]
1157 fn accessor_elixir_chunks_length_uses_length_function() {
1158 let expr = StreamingFieldResolver::accessor("chunks.length", "elixir", "chunks").unwrap();
1159 assert_eq!(expr, "length(chunks)", "elixir chunks.length: {expr}");
1160 }
1161
1162 #[test]
1163 fn accessor_elixir_stream_content_uses_pipe() {
1164 let expr = StreamingFieldResolver::accessor("stream_content", "elixir", "chunks").unwrap();
1165 assert!(expr.contains("|> Enum.join"), "elixir stream_content: {expr}");
1166 assert!(expr.contains("|> Enum.map"), "elixir stream_content: {expr}");
1167 assert!(
1169 !expr.contains("choices[0]"),
1170 "elixir stream_content must not use bracket access on list: {expr}"
1171 );
1172 assert!(
1173 expr.contains("Enum.at("),
1174 "elixir stream_content must use Enum.at for list index: {expr}"
1175 );
1176 }
1177
1178 #[test]
1179 fn accessor_elixir_stream_complete_uses_list_last() {
1180 let expr = StreamingFieldResolver::accessor("stream_complete", "elixir", "chunks").unwrap();
1181 assert!(expr.contains("List.last(chunks)"), "elixir stream_complete: {expr}");
1182 assert!(expr.contains("finish_reason != nil"), "elixir stream_complete: {expr}");
1183 assert!(
1185 !expr.contains("choices[0]"),
1186 "elixir stream_complete must not use bracket access on list: {expr}"
1187 );
1188 assert!(
1189 expr.contains("Enum.at("),
1190 "elixir stream_complete must use Enum.at for list index: {expr}"
1191 );
1192 }
1193
1194 #[test]
1195 fn accessor_elixir_finish_reason_uses_list_last() {
1196 let expr = StreamingFieldResolver::accessor("finish_reason", "elixir", "chunks").unwrap();
1197 assert!(expr.contains("List.last(chunks)"), "elixir finish_reason: {expr}");
1198 assert!(expr.contains("finish_reason"), "elixir finish_reason: {expr}");
1199 assert!(
1201 !expr.contains("choices[0]"),
1202 "elixir finish_reason must not use bracket access on list: {expr}"
1203 );
1204 assert!(
1205 expr.contains("Enum.at("),
1206 "elixir finish_reason must use Enum.at for list index: {expr}"
1207 );
1208 }
1209
1210 #[test]
1211 fn collect_snippet_elixir_uses_enum_to_list() {
1212 let snip = StreamingFieldResolver::collect_snippet("elixir", "result", "chunks").unwrap();
1213 assert!(snip.contains("Enum.to_list(result)"), "elixir: {snip}");
1214 assert!(snip.contains("chunks ="), "elixir: {snip}");
1215 }
1216
1217 #[test]
1218 fn collect_snippet_rust_uses_tokio_stream() {
1219 let snip = StreamingFieldResolver::collect_snippet("rust", "result", "chunks").unwrap();
1220 assert!(snip.contains("tokio_stream::StreamExt::collect"), "rust: {snip}");
1221 assert!(snip.contains("let chunks"), "rust: {snip}");
1222 assert!(snip.contains(".expect("), "rust must unwrap Result items: {snip}");
1224 }
1225
1226 #[test]
1227 fn collect_snippet_go_drains_channel() {
1228 let snip = StreamingFieldResolver::collect_snippet("go", "stream", "chunks").unwrap();
1229 assert!(snip.contains("for chunk := range stream"), "go: {snip}");
1230 }
1231
1232 #[test]
1233 fn collect_snippet_java_uses_iterator() {
1234 let snip = StreamingFieldResolver::collect_snippet("java", "result", "chunks").unwrap();
1235 assert!(
1238 snip.contains(".iterator()"),
1239 "java snippet must call .iterator() on stream: {snip}"
1240 );
1241 assert!(snip.contains("hasNext()"), "java: {snip}");
1242 assert!(snip.contains(".next()"), "java: {snip}");
1243 }
1244
1245 #[test]
1246 fn collect_snippet_php_decodes_json_or_iterates() {
1247 let snip = StreamingFieldResolver::collect_snippet("php", "result", "chunks").unwrap();
1248 assert!(snip.contains("json_decode"), "php must decode JSON: {snip}");
1253 assert!(
1254 snip.contains("iterator_to_array"),
1255 "php must keep iterator_to_array fallback: {snip}"
1256 );
1257 assert!(snip.contains("$chunks ="), "php must bind $chunks: {snip}");
1258 }
1259
1260 #[test]
1261 fn collect_snippet_node_uses_for_await() {
1262 let snip = StreamingFieldResolver::collect_snippet("node", "result", "chunks").unwrap();
1263 assert!(snip.contains("for await"), "node: {snip}");
1264 }
1265
1266 #[test]
1267 fn collect_snippet_python_uses_async_for() {
1268 let snip = StreamingFieldResolver::collect_snippet("python", "result", "chunks").unwrap();
1269 assert!(snip.contains("async for chunk in result"), "python: {snip}");
1270 assert!(snip.contains("chunks.append(chunk)"), "python: {snip}");
1271 }
1272
1273 #[test]
1274 fn accessor_stream_content_python_uses_join() {
1275 let expr = StreamingFieldResolver::accessor("stream_content", "python", "chunks").unwrap();
1276 assert!(expr.contains("\"\".join("), "python stream_content: {expr}");
1277 assert!(expr.contains("c.choices"), "python stream_content: {expr}");
1278 }
1279
1280 #[test]
1281 fn accessor_stream_complete_python_uses_finish_reason() {
1282 let expr = StreamingFieldResolver::accessor("stream_complete", "python", "chunks").unwrap();
1283 assert!(
1284 expr.contains("finish_reason is not None"),
1285 "python stream_complete: {expr}"
1286 );
1287 }
1288
1289 #[test]
1290 fn accessor_finish_reason_python_uses_last_chunk() {
1291 let expr = StreamingFieldResolver::accessor("finish_reason", "python", "chunks").unwrap();
1292 assert!(expr.contains("chunks[-1]"), "python finish_reason: {expr}");
1293 assert!(
1295 expr.starts_with("(str(") || expr.contains("str(chunks"),
1296 "python finish_reason must wrap in str(): {expr}"
1297 );
1298 }
1299
1300 #[test]
1301 fn accessor_tool_calls_python_uses_list_comprehension() {
1302 let expr = StreamingFieldResolver::accessor("tool_calls", "python", "chunks").unwrap();
1303 assert!(expr.contains("for c in chunks"), "python tool_calls: {expr}");
1304 assert!(expr.contains("tool_calls"), "python tool_calls: {expr}");
1305 }
1306
1307 #[test]
1308 fn accessor_usage_python_uses_last_chunk() {
1309 let expr = StreamingFieldResolver::accessor("usage", "python", "chunks").unwrap();
1310 assert!(
1311 expr.contains("chunks[-1].usage"),
1312 "python usage: expected chunks[-1].usage, got: {expr}"
1313 );
1314 }
1315
1316 #[test]
1317 fn accessor_usage_total_tokens_does_not_route_via_chunks() {
1318 assert!(StreamingFieldResolver::accessor("usage.total_tokens", "python", "chunks").is_none());
1322 }
1323
1324 #[test]
1325 fn accessor_unknown_field_returns_none() {
1326 assert_eq!(
1327 StreamingFieldResolver::accessor("nonexistent_field", "rust", "chunks"),
1328 None
1329 );
1330 }
1331
1332 #[test]
1337 fn is_streaming_virtual_field_recognizes_deep_tool_calls_paths() {
1338 assert!(
1339 is_streaming_virtual_field("tool_calls[0].function.name"),
1340 "tool_calls[0].function.name should be recognized"
1341 );
1342 assert!(
1343 is_streaming_virtual_field("tool_calls[0].id"),
1344 "tool_calls[0].id should be recognized"
1345 );
1346 assert!(
1347 is_streaming_virtual_field("tool_calls[1].function.arguments"),
1348 "tool_calls[1].function.arguments should be recognized"
1349 );
1350 assert!(is_streaming_virtual_field("tool_calls"));
1352 assert!(!is_streaming_virtual_field("tool_calls_extra.name"));
1354 assert!(!is_streaming_virtual_field("nonexistent[0].field"));
1355 }
1356
1357 #[test]
1364 fn deep_tool_calls_function_name_snapshot_rust_kotlin_ts() {
1365 let field = "tool_calls[0].function.name";
1366
1367 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1368 assert!(
1372 rust.contains(".nth(0)"),
1373 "rust deep tool_calls: expected .nth(0) iterator index, got: {rust}"
1374 );
1375 assert!(
1376 rust.contains("x.function.as_ref()"),
1377 "rust deep tool_calls: expected Option-aware function access, got: {rust}"
1378 );
1379 assert!(
1380 rust.contains("x.name.as_deref()"),
1381 "rust deep tool_calls: expected Option-aware name leaf, got: {rust}"
1382 );
1383 assert!(
1384 !rust.contains("// skipped"),
1385 "rust deep tool_calls: must not emit skip comment, got: {rust}"
1386 );
1387
1388 let kotlin = StreamingFieldResolver::accessor(field, "kotlin", "chunks").unwrap();
1389 assert!(
1391 kotlin.contains(".first()"),
1392 "kotlin deep tool_calls: expected .first() for index 0, got: {kotlin}"
1393 );
1394 assert!(
1395 kotlin.contains(".function()"),
1396 "kotlin deep tool_calls: expected .function() method call, got: {kotlin}"
1397 );
1398 assert!(
1399 kotlin.contains(".name()"),
1400 "kotlin deep tool_calls: expected .name() method call, got: {kotlin}"
1401 );
1402
1403 let ts = StreamingFieldResolver::accessor(field, "node", "chunks").unwrap();
1404 assert!(
1406 ts.contains("[0]"),
1407 "ts/node deep tool_calls: expected [0] index, got: {ts}"
1408 );
1409 assert!(
1410 ts.contains(".function"),
1411 "ts/node deep tool_calls: expected .function segment, got: {ts}"
1412 );
1413 assert!(
1414 ts.contains(".name"),
1415 "ts/node deep tool_calls: expected .name segment, got: {ts}"
1416 );
1417 }
1418
1419 #[test]
1420 fn deep_tool_calls_id_snapshot_all_langs() {
1421 let field = "tool_calls[0].id";
1422
1423 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1424 assert!(rust.contains(".nth(0)"), "rust: {rust}");
1425 assert!(rust.contains("x.id.as_deref()"), "rust: {rust}");
1426
1427 let go = StreamingFieldResolver::accessor(field, "go", "chunks").unwrap();
1428 assert!(go.contains("[0]"), "go: {go}");
1429 assert!(go.contains(".ID"), "go: expected .ID initialism, got: {go}");
1431
1432 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1433 assert!(python.contains("[0]"), "python: {python}");
1434 assert!(python.contains(".id"), "python: {python}");
1435
1436 let php = StreamingFieldResolver::accessor(field, "php", "chunks").unwrap();
1437 assert!(php.contains("[0]"), "php: {php}");
1438 assert!(php.contains("->id"), "php: expected ->id, got: {php}");
1439
1440 let java = StreamingFieldResolver::accessor(field, "java", "chunks").unwrap();
1441 assert!(java.contains(".get(0)"), "java: expected .get(0), got: {java}");
1442 assert!(java.contains(".id()"), "java: expected .id() method call, got: {java}");
1443
1444 let csharp = StreamingFieldResolver::accessor(field, "csharp", "chunks").unwrap();
1445 assert!(csharp.contains("[0]"), "csharp: {csharp}");
1446 assert!(
1447 csharp.contains(".Id"),
1448 "csharp: expected .Id (PascalCase), got: {csharp}"
1449 );
1450
1451 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1452 assert!(elixir.contains("Enum.at("), "elixir: expected Enum.at(, got: {elixir}");
1453 assert!(elixir.contains(".id"), "elixir: {elixir}");
1454 }
1455
1456 #[test]
1457 fn deep_tool_calls_function_name_snapshot_python_elixir_zig() {
1458 let field = "tool_calls[0].function.name";
1459
1460 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1461 assert!(python.contains("[0]"), "python: {python}");
1462 assert!(python.contains(".function"), "python: {python}");
1463 assert!(python.contains(".name"), "python: {python}");
1464
1465 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1466 assert!(elixir.contains("Enum.at("), "elixir: {elixir}");
1468 assert!(elixir.contains(".function"), "elixir: {elixir}");
1469 assert!(elixir.contains(".name"), "elixir: {elixir}");
1470
1471 assert!(
1475 StreamingFieldResolver::accessor(field, "zig", "chunks").is_none(),
1476 "zig: expected None for deep tool_calls path"
1477 );
1478 }
1479
1480 #[test]
1481 fn parse_tail_parses_index_then_field_segments() {
1482 let segs = parse_tail("[0].function.name");
1483 assert_eq!(segs.len(), 3, "expected 3 segments, got: {segs:?}");
1484 assert_eq!(segs[0], TailSeg::Index(0));
1485 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1486 assert_eq!(segs[2], TailSeg::Field("name".to_string()));
1487 }
1488
1489 #[test]
1490 fn parse_tail_parses_simple_index_field() {
1491 let segs = parse_tail("[0].id");
1492 assert_eq!(segs.len(), 2, "expected 2 segments, got: {segs:?}");
1493 assert_eq!(segs[0], TailSeg::Index(0));
1494 assert_eq!(segs[1], TailSeg::Field("id".to_string()));
1495 }
1496
1497 #[test]
1498 fn parse_tail_handles_nonzero_index() {
1499 let segs = parse_tail("[2].function.arguments");
1500 assert_eq!(segs[0], TailSeg::Index(2));
1501 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1502 assert_eq!(segs[2], TailSeg::Field("arguments".to_string()));
1503 }
1504
1505 #[test]
1510 fn accessor_chunks_length_swift_uses_count() {
1511 let swift = StreamingFieldResolver::accessor("chunks.length", "swift", "chunks").unwrap();
1512 assert_eq!(swift, "chunks.count", "swift chunks.length: {swift}");
1513 }
1514
1515 #[test]
1516 fn accessor_stream_content_swift_uses_swift_closures() {
1517 let expr = StreamingFieldResolver::accessor("stream_content", "swift", "chunks").unwrap();
1518 assert!(
1520 expr.contains("{ c in"),
1521 "swift stream_content must use Swift closure syntax, got: {expr}"
1522 );
1523 assert!(
1524 !expr.contains("=>"),
1525 "swift stream_content must not contain JS arrow `=>`, got: {expr}"
1526 );
1527 assert!(
1529 expr.contains("choices()"),
1530 "swift stream_content must use .choices() method call, got: {expr}"
1531 );
1532 assert!(
1533 expr.contains("delta()"),
1534 "swift stream_content must use .delta() method call, got: {expr}"
1535 );
1536 assert!(
1537 expr.contains("content()"),
1538 "swift stream_content must use .content() method call, got: {expr}"
1539 );
1540 assert!(
1541 expr.contains(".toString()"),
1542 "swift stream_content must convert RustString via .toString(), got: {expr}"
1543 );
1544 assert!(
1545 expr.contains(".joined()"),
1546 "swift stream_content must join with .joined(), got: {expr}"
1547 );
1548 assert!(
1550 !expr.contains(".length"),
1551 "swift stream_content must not use JS .length, got: {expr}"
1552 );
1553 assert!(
1554 !expr.contains(".join("),
1555 "swift stream_content must not use JS .join(, got: {expr}"
1556 );
1557 }
1558
1559 #[test]
1560 fn accessor_stream_complete_swift_uses_swift_syntax() {
1561 let expr = StreamingFieldResolver::accessor("stream_complete", "swift", "chunks").unwrap();
1562 assert!(
1564 expr.contains("isEmpty"),
1565 "swift stream_complete must use .isEmpty, got: {expr}"
1566 );
1567 assert!(
1568 expr.contains(".last!"),
1569 "swift stream_complete must use .last!, got: {expr}"
1570 );
1571 assert!(
1572 expr.contains("choices()"),
1573 "swift stream_complete must use .choices() method call, got: {expr}"
1574 );
1575 assert!(
1576 expr.contains("finish_reason()"),
1577 "swift stream_complete must use .finish_reason(), got: {expr}"
1578 );
1579 assert!(
1580 !expr.contains(".length"),
1581 "swift stream_complete must not use JS .length, got: {expr}"
1582 );
1583 assert!(
1584 !expr.contains("!= null"),
1585 "swift stream_complete must not use JS `!= null`, got: {expr}"
1586 );
1587 }
1588
1589 #[test]
1590 fn accessor_tool_calls_swift_uses_swift_flatmap() {
1591 let expr = StreamingFieldResolver::accessor("tool_calls", "swift", "chunks").unwrap();
1592 assert!(
1594 !expr.contains("=>"),
1595 "swift tool_calls must not contain JS arrow `=>`, got: {expr}"
1596 );
1597 assert!(
1598 expr.contains("flatMap"),
1599 "swift tool_calls must use flatMap, got: {expr}"
1600 );
1601 assert!(
1602 expr.contains("choices()"),
1603 "swift tool_calls must use .choices() method call, got: {expr}"
1604 );
1605 assert!(
1606 expr.contains("delta()"),
1607 "swift tool_calls must use .delta() method call, got: {expr}"
1608 );
1609 assert!(
1610 expr.contains("tool_calls()"),
1611 "swift tool_calls must use .tool_calls() method call, got: {expr}"
1612 );
1613 }
1614
1615 #[test]
1616 fn accessor_tool_calls_deep_path_swift_uses_method_calls_with_optional_chain() {
1617 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "swift", "chunks").unwrap();
1621 assert!(
1622 expr.contains("function()"),
1623 "swift deep tool_calls must use .function() method call, got: {expr}"
1624 );
1625 assert!(
1626 expr.contains("name()"),
1627 "swift deep tool_calls must use .name() method call, got: {expr}"
1628 );
1629 assert!(
1630 expr.contains(".toString()"),
1631 "swift deep tool_calls must convert RustString via .toString(), got: {expr}"
1632 );
1633 assert!(
1634 !expr.contains("=>"),
1635 "swift deep tool_calls must not use JS arrow syntax, got: {expr}"
1636 );
1637 }
1638
1639 #[test]
1640 fn accessor_finish_reason_swift_uses_swift_syntax() {
1641 let expr = StreamingFieldResolver::accessor("finish_reason", "swift", "chunks").unwrap();
1642 assert!(
1644 expr.contains("isEmpty"),
1645 "swift finish_reason must use .isEmpty, got: {expr}"
1646 );
1647 assert!(
1648 expr.contains(".last!"),
1649 "swift finish_reason must use .last!, got: {expr}"
1650 );
1651 assert!(
1652 expr.contains("finish_reason()"),
1653 "swift finish_reason must use .finish_reason() method call, got: {expr}"
1654 );
1655 assert!(
1656 expr.contains(".toString()"),
1657 "swift finish_reason must convert RustString via .toString(), got: {expr}"
1658 );
1659 assert!(
1660 !expr.contains("undefined"),
1661 "swift finish_reason must not use JS `undefined`, got: {expr}"
1662 );
1663 assert!(
1664 !expr.contains(".length"),
1665 "swift finish_reason must not use JS .length, got: {expr}"
1666 );
1667 }
1668
1669 #[test]
1670 fn accessor_usage_swift_uses_swift_syntax() {
1671 let expr = StreamingFieldResolver::accessor("usage", "swift", "chunks").unwrap();
1672 assert!(expr.contains("isEmpty"), "swift usage must use .isEmpty, got: {expr}");
1674 assert!(expr.contains(".last!"), "swift usage must use .last!, got: {expr}");
1675 assert!(
1676 expr.contains("usage()"),
1677 "swift usage must use .usage() method call, got: {expr}"
1678 );
1679 assert!(
1680 !expr.contains("undefined"),
1681 "swift usage must not use JS `undefined`, got: {expr}"
1682 );
1683 assert!(
1684 !expr.contains(".length"),
1685 "swift usage must not use JS .length, got: {expr}"
1686 );
1687 }
1688
1689 #[test]
1694 fn kotlin_android_collect_snippet_uses_flow_to_list() {
1695 let snip = StreamingFieldResolver::collect_snippet("kotlin_android", "result", "chunks").unwrap();
1696 assert!(
1698 snip.contains("result.toList()"),
1699 "kotlin_android collect must use Flow.toList(), got: {snip}"
1700 );
1701 assert!(
1702 !snip.contains("asSequence()"),
1703 "kotlin_android collect must NOT use asSequence(), got: {snip}"
1704 );
1705 }
1706
1707 #[test]
1708 fn kotlin_android_stream_content_uses_property_access() {
1709 let expr = StreamingFieldResolver::accessor("stream_content", "kotlin_android", "chunks").unwrap();
1710 assert!(
1711 expr.contains(".choices"),
1712 "kotlin_android stream_content must use .choices property, got: {expr}"
1713 );
1714 assert!(
1715 !expr.contains(".choices()"),
1716 "kotlin_android stream_content must NOT use .choices() getter, got: {expr}"
1717 );
1718 assert!(
1719 expr.contains(".delta"),
1720 "kotlin_android stream_content must use .delta property, got: {expr}"
1721 );
1722 assert!(
1723 !expr.contains(".delta()"),
1724 "kotlin_android stream_content must NOT use .delta() getter, got: {expr}"
1725 );
1726 assert!(
1727 expr.contains(".content"),
1728 "kotlin_android stream_content must use .content property, got: {expr}"
1729 );
1730 assert!(
1731 !expr.contains(".content()"),
1732 "kotlin_android stream_content must NOT use .content() getter, got: {expr}"
1733 );
1734 }
1735
1736 #[test]
1737 fn kotlin_android_finish_reason_uses_name_lowercase_not_get_value() {
1738 let expr = StreamingFieldResolver::accessor("finish_reason", "kotlin_android", "chunks").unwrap();
1739 assert!(
1740 expr.contains(".finishReason"),
1741 "kotlin_android finish_reason must use .finishReason property, got: {expr}"
1742 );
1743 assert!(
1744 !expr.contains(".finishReason()"),
1745 "kotlin_android finish_reason must NOT use .finishReason() getter, got: {expr}"
1746 );
1747 assert!(
1748 expr.contains(".name"),
1749 "kotlin_android finish_reason must use .name for enum wire value, got: {expr}"
1750 );
1751 assert!(
1752 expr.contains(".lowercase()"),
1753 "kotlin_android finish_reason must use .lowercase(), got: {expr}"
1754 );
1755 assert!(
1756 !expr.contains(".getValue()"),
1757 "kotlin_android finish_reason must NOT use .getValue(), got: {expr}"
1758 );
1759 }
1760
1761 #[test]
1762 fn kotlin_android_usage_uses_property_access() {
1763 let expr = StreamingFieldResolver::accessor("usage", "kotlin_android", "chunks").unwrap();
1764 assert!(
1765 expr.contains(".usage"),
1766 "kotlin_android usage must use .usage property, got: {expr}"
1767 );
1768 assert!(
1769 !expr.contains(".usage()"),
1770 "kotlin_android usage must NOT use .usage() getter, got: {expr}"
1771 );
1772 }
1773
1774 #[test]
1775 fn kotlin_android_deep_tool_calls_uses_property_access() {
1776 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "kotlin_android", "chunks").unwrap();
1777 assert!(
1778 expr.contains(".function"),
1779 "kotlin_android deep tool_calls must use .function property, got: {expr}"
1780 );
1781 assert!(
1782 !expr.contains(".function()"),
1783 "kotlin_android deep tool_calls must NOT use .function() getter, got: {expr}"
1784 );
1785 assert!(
1786 expr.contains(".name"),
1787 "kotlin_android deep tool_calls must use .name property, got: {expr}"
1788 );
1789 assert!(
1790 !expr.contains(".name()"),
1791 "kotlin_android deep tool_calls must NOT use .name() getter, got: {expr}"
1792 );
1793 }
1794}