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> {
161 Self::accessor_with_module_qualifier(field, lang, chunks_var, None)
162 }
163
164 pub fn accessor_with_module_qualifier(
174 field: &str,
175 lang: &str,
176 chunks_var: &str,
177 module_qualifier: Option<&str>,
178 ) -> Option<String> {
179 Self::accessor_with_streaming_context(field, lang, chunks_var, module_qualifier, Some("CrawlEvent"))
182 }
183
184 pub fn accessor_with_streaming_context(
194 field: &str,
195 lang: &str,
196 chunks_var: &str,
197 module_qualifier: Option<&str>,
198 item_type: Option<&str>,
199 ) -> Option<String> {
200 match field {
201 "chunks" => Some(match lang {
202 "zig" => format!("{chunks_var}.items"),
204 "php" => format!("${chunks_var}"),
207 _ => chunks_var.to_string(),
208 }),
209
210 "chunks.length" => Some(match lang {
211 "rust" => format!("{chunks_var}.len()"),
212 "go" => format!("len({chunks_var})"),
213 "python" => format!("len({chunks_var})"),
214 "php" => format!("count(${chunks_var})"),
215 "elixir" => format!("length({chunks_var})"),
216 "kotlin" => format!("{chunks_var}.size"),
218 "zig" => format!("{chunks_var}.items.len"),
220 "swift" => format!("{chunks_var}.count"),
222 _ => format!("{chunks_var}.length"),
224 }),
225
226 "stream_content" => Some(match lang {
227 "rust" => {
228 format!(
229 "{chunks_var}.iter().map(|c| c.choices.first().and_then(|ch| ch.delta.content.as_deref()).unwrap_or(\"\")).collect::<String>()"
230 )
231 }
232 "go" => {
233 format!(
235 "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 }}()"
236 )
237 }
238 "java" => {
239 format!(
240 "{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())"
241 )
242 }
243 "php" => {
244 format!("implode('', array_map(fn($c) => $c->choices[0]->delta->content ?? '', ${chunks_var}))")
245 }
246 "kotlin" => {
247 format!(
250 "{chunks_var}.joinToString(\"\") {{ it.choices()?.firstOrNull()?.delta()?.content() ?: \"\" }}"
251 )
252 }
253 "kotlin_android" => {
254 format!("{chunks_var}.joinToString(\"\") {{ it.choices?.firstOrNull()?.delta?.content ?: \"\" }}")
256 }
257 "elixir" => {
258 format!(
262 "{chunks_var} |> Enum.map(fn c -> (Enum.at(c.choices, 0) || %{{}}) |> Map.get(:delta, %{{}}) |> Map.get(:content, \"\") end) |> Enum.join(\"\")"
263 )
264 }
265 "python" => {
266 format!("\"\".join(c.choices[0].delta.content or \"\" for c in {chunks_var} if c.choices)")
267 }
268 "zig" => {
269 format!("{chunks_var}_content.items")
272 }
273 "swift" => {
279 format!(
280 "{chunks_var}.map {{ c in c.choices.first.flatMap {{ ch in ch.delta.content }} ?? \"\" }}.joined()"
281 )
282 }
283 _ => {
285 format!("{chunks_var}.map((c: any) => c.choices?.[0]?.delta?.content ?? '').join('')")
286 }
287 }),
288
289 "stream_complete" => Some(match lang {
290 "rust" => {
291 format!(
292 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).is_some()"
293 )
294 }
295 "go" => {
296 format!(
297 "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 }}()"
298 )
299 }
300 "java" => {
301 format!(
302 "!{chunks_var}.isEmpty() && {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().flatMap(ch -> java.util.Optional.ofNullable(ch.finishReason())).isPresent()"
303 )
304 }
305 "php" => {
306 format!("!empty(${chunks_var}) && isset(end(${chunks_var})->choices[0]->finishReason)")
310 }
311 "kotlin" => {
312 format!(
314 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices()?.firstOrNull()?.finishReason() != null"
315 )
316 }
317 "kotlin_android" => {
318 format!(
320 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices?.firstOrNull()?.finishReason != null"
321 )
322 }
323 "python" => {
324 format!("bool({chunks_var}) and {chunks_var}[-1].choices[0].finish_reason is not None")
325 }
326 "elixir" => {
327 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason != nil")
328 }
329 "zig" => {
332 format!("{chunks_var}.items.len > 0")
333 }
334 "swift" => {
338 format!("!{chunks_var}.isEmpty && {chunks_var}.last!.choices.first?.finishReason != nil")
339 }
340 _ => {
342 format!(
343 "{chunks_var}.length > 0 && {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason != null"
344 )
345 }
346 }),
347
348 "no_chunks_after_done" => Some(match lang {
352 "rust" => "true".to_string(),
353 "go" => "true".to_string(),
354 "java" => "true".to_string(),
355 "php" => "true".to_string(),
356 _ => "true".to_string(),
357 }),
358
359 "stream.has_page_event" => item_type
374 .and_then(|ty| has_event_variant_accessor(lang, chunks_var, EventVariant::Page, ty, module_qualifier)),
375 "stream.has_error_event" => item_type
376 .and_then(|ty| has_event_variant_accessor(lang, chunks_var, EventVariant::Error, ty, module_qualifier)),
377 "stream.has_complete_event" => item_type.and_then(|ty| {
378 has_event_variant_accessor(lang, chunks_var, EventVariant::Complete, ty, module_qualifier)
379 }),
380
381 "stream.event_count_min" => Some(match lang {
385 "java" => format!("{chunks_var}.size()"),
386 "go" => format!("len({chunks_var})"),
387 "php" => format!("count(${chunks_var})"),
388 "kotlin" | "kotlin_android" => format!("{chunks_var}.size"),
389 "python" => format!("len({chunks_var})"),
390 "rust" => format!("{chunks_var}.len()"),
391 "node" | "typescript" | "wasm" => format!("{chunks_var}.length"),
392 "swift" => format!("{chunks_var}.count"),
393 "zig" => format!("{chunks_var}.items.len"),
394 "ruby" => format!("{chunks_var}.length"),
395 "elixir" => format!("length({chunks_var})"),
396 "c" => format!("vlen({chunks_var})"),
397 _ => format!("{chunks_var}.length"),
398 }),
399
400 "tool_calls" => Some(match lang {
401 "rust" => {
402 format!(
403 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten())).collect::<Vec<_>>()"
404 )
405 }
406 "go" => {
407 format!(
411 "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 }}()"
412 )
413 }
414 "java" => {
415 format!(
416 "{chunks_var}.stream().flatMap(c -> c.choices().stream()).flatMap(ch -> ch.delta().toolCalls() != null ? ch.delta().toolCalls().stream() : java.util.stream.Stream.empty()).toList()"
417 )
418 }
419 "php" => {
420 format!(
423 "array_merge(...array_map(fn($c) => $c->choices[0]->delta->toolCalls ?? [], ${chunks_var}))"
424 )
425 }
426 "kotlin" => {
427 format!(
429 "{chunks_var}.flatMap {{ c -> c.choices()?.flatMap {{ ch -> ch.delta()?.toolCalls() ?: emptyList() }} ?: emptyList() }}"
430 )
431 }
432 "kotlin_android" => {
433 format!(
435 "{chunks_var}.flatMap {{ c -> c.choices?.flatMap {{ ch -> ch.delta?.toolCalls ?: emptyList() }} ?: emptyList() }}"
436 )
437 }
438 "python" => {
439 format!(
440 "[t for c in {chunks_var} for ch in (c.choices or []) for t in (ch.delta.tool_calls or [])]"
441 )
442 }
443 "elixir" => {
444 format!(
445 "{chunks_var} |> Enum.flat_map(fn c -> (List.first(c.choices) || %{{}}).delta |> Map.get(:tool_calls, []) end)"
446 )
447 }
448 "zig" => {
450 format!("{chunks_var}.items")
451 }
452 "swift" => {
456 format!(
457 "{chunks_var}.flatMap {{ c -> [StreamToolCall] in guard let ch = c.choices.first, let tcs = ch.delta.toolCalls else {{ return [] }}; return tcs }}"
458 )
459 }
460 _ => {
461 format!("{chunks_var}.flatMap((c: any) => c.choices?.[0]?.delta?.toolCalls ?? [])")
462 }
463 }),
464
465 "finish_reason" => Some(match lang {
466 "rust" => {
467 format!(
470 "{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()"
471 )
472 }
473 "go" => {
474 format!(
477 "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 \"\" }}()"
478 )
479 }
480 "java" => {
481 format!(
485 "({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))"
486 )
487 }
488 "php" => {
489 format!("(!empty(${chunks_var}) ? (end(${chunks_var})->choices[0]->finishReason ?? null) : null)")
492 }
493 "kotlin" => {
494 format!(
497 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices()?.firstOrNull()?.finishReason()?.getValue())"
498 )
499 }
500 "kotlin_android" => {
501 format!(
503 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices?.firstOrNull()?.finishReason?.name?.lowercase())"
504 )
505 }
506 "python" => {
507 format!(
511 "(str({chunks_var}[-1].choices[0].finish_reason) if {chunks_var} and {chunks_var}[-1].choices else None)"
512 )
513 }
514 "elixir" => {
515 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason")
516 }
517 "zig" => {
520 format!(
521 "(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 \"\"; }})"
522 )
523 }
524 "swift" => {
529 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.choices.first?.finishReason?.rawValue)")
530 }
531 _ => {
532 format!(
533 "{chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason : undefined"
534 )
535 }
536 }),
537
538 "usage" => Some(match lang {
543 "python" => {
544 format!("({chunks_var}[-1].usage if {chunks_var} else None)")
548 }
549 "rust" => {
550 format!("{chunks_var}.last().and_then(|c| c.usage.as_ref())")
551 }
552 "go" => {
553 format!(
554 "func() interface{{}} {{ if len({chunks_var}) == 0 {{ return nil }}; return {chunks_var}[len({chunks_var})-1].Usage }}()"
555 )
556 }
557 "java" => {
558 format!("({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).usage())")
559 }
560 "kotlin" => {
561 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage())")
562 }
563 "kotlin_android" => {
564 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage)")
566 }
567 "php" => {
568 format!("(!empty(${chunks_var}) ? end(${chunks_var})->usage ?? null : null)")
569 }
570 "elixir" => {
571 format!("(if length({chunks_var}) > 0, do: List.last({chunks_var}).usage, else: nil)")
572 }
573 "swift" => {
576 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.usage)")
577 }
578 _ => {
579 format!("({chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].usage : undefined)")
580 }
581 }),
582
583 _ => {
584 if let Some((root, tail)) = split_streaming_deep_path(field) {
588 if lang == "rust" && root == "tool_calls" {
592 return Some(render_rust_tool_calls_deep(chunks_var, tail));
593 }
594 if lang == "swift" && root == "tool_calls" {
598 let root_expr = Self::accessor(root, lang, chunks_var)?;
599 return Some(render_swift_tool_calls_deep(&root_expr, tail));
600 }
601 if lang == "zig" && root == "tool_calls" {
608 return None;
609 }
610 let root_expr = Self::accessor(root, lang, chunks_var)?;
611 Some(render_deep_tail(&root_expr, tail, lang))
612 } else {
613 None
614 }
615 }
616 }
617 }
618
619 pub fn collect_snippet(lang: &str, stream_var: &str, chunks_var: &str) -> Option<String> {
625 Self::collect_snippet_typed(lang, stream_var, chunks_var, None)
626 }
627
628 pub fn collect_snippet_typed(
633 lang: &str,
634 stream_var: &str,
635 chunks_var: &str,
636 item_type: Option<&str>,
637 ) -> Option<String> {
638 let item_type = item_type.unwrap_or("ChatCompletionChunk");
639 match lang {
640 "rust" => Some(format!(
641 "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();"
642 )),
643 "go" => Some(format!(
644 "var {chunks_var} []pkg.{item_type}\n\tfor chunk := range {stream_var} {{\n\t\t{chunks_var} = append({chunks_var}, chunk)\n\t}}"
645 )),
646 "java" => Some(format!(
647 "var {chunks_var} = new java.util.ArrayList<{item_type}>();\n var _it = {stream_var}.iterator();\n while (_it.hasNext()) {{ {chunks_var}.add(_it.next()); }}"
648 )),
649 "php" => Some(format!(
659 "$__camel = function ($v) use (&$__camel) {{ \
660 if (is_array($v)) {{ \
661 $out = []; \
662 foreach ($v as $k => $vv) {{ \
663 $key = is_string($k) ? lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))) : $k; \
664 $out[$key] = $__camel($vv); \
665 }} \
666 return (array_keys($out) === range(0, count($out) - 1)) ? $out : (object) $out; \
667 }} \
668 if (is_object($v)) {{ \
669 $out = new \\stdClass(); \
670 foreach (get_object_vars($v) as $k => $vv) {{ \
671 $key = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))); \
672 $out->{{$key}} = $__camel($vv); \
673 }} \
674 return $out; \
675 }} \
676 return $v; \
677 }};\n \
678 $__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 \
679 ${chunks_var} = is_string(${stream_var}) \
680 ? array_map($__decode_chunk, (array)(json_decode(${stream_var}, true) ?: [])) \
681 : (is_array(${stream_var}) \
682 ? array_map($__decode_chunk, ${stream_var}) \
683 : array_map($__decode_chunk, iterator_to_array(${stream_var})));"
684 )),
685 "python" => Some(format!(
686 "{chunks_var} = []\n async for chunk in {stream_var}:\n {chunks_var}.append(chunk)"
687 )),
688 "kotlin" => {
689 Some(format!("val {chunks_var} = {stream_var}.asSequence().toList()"))
692 }
693 "kotlin_android" => {
694 Some(format!("val {chunks_var} = {stream_var}.toList()"))
697 }
698 "elixir" => Some(format!("{chunks_var} = Enum.to_list({stream_var})")),
699 "wasm" => Some(format!(
706 "const {chunks_var}: any[] = [];\n while (true) {{ const _chunk = await {stream_var}.next(); if (_chunk == null) break; {chunks_var}.push(_chunk); }}"
707 )),
708 "node" | "typescript" => Some(format!(
709 "const {chunks_var}: any[] = [];\n for await (const _chunk of {stream_var}) {{ {chunks_var}.push(_chunk); }}"
710 )),
711 "swift" => {
712 Some(format!(
717 "var {chunks_var}: [ChatCompletionChunk] = []\n for try await _chunk in {stream_var} {{ {chunks_var}.append(_chunk) }}"
718 ))
719 }
720 "zig" => Some(Self::collect_snippet_zig(stream_var, chunks_var, "module", "ffi")),
721 _ => None,
722 }
723 }
724
725 pub fn collect_snippet_zig(stream_var: &str, chunks_var: &str, module_name: &str, ffi_prefix: &str) -> String {
727 let stream_next = format!("{ffi_prefix}_default_client_chat_stream_next");
728 let chunk_to_json = format!("{ffi_prefix}_chat_completion_chunk_to_json");
729 let chunk_free = format!("{ffi_prefix}_chat_completion_chunk_free");
730 let free_string = format!("{ffi_prefix}_free_string");
731
732 format!(
739 concat!(
740 "var {chunks_var}: std.ArrayList([]u8) = .empty;
741",
742 " defer {{
743",
744 " for ({chunks_var}.items) |_cj| std.heap.c_allocator.free(_cj);
745",
746 " {chunks_var}.deinit(std.heap.c_allocator);
747",
748 " }}
749",
750 " var {chunks_var}_content: std.ArrayList(u8) = .empty;
751",
752 " defer {chunks_var}_content.deinit(std.heap.c_allocator);
753",
754 " while (true) {{
755",
756 " const _nc = {module_name}.c.{stream_next}({stream_var});
757",
758 " if (_nc == null) break;
759",
760 " const _np = {module_name}.c.{chunk_to_json}(_nc);
761",
762 " {module_name}.c.{chunk_free}(_nc);
763",
764 " if (_np == null) continue;
765",
766 " const _ns = std.mem.span(_np);
767",
768 " const _nj = try std.heap.c_allocator.dupe(u8, _ns);
769",
770 " {module_name}.c.{free_string}(_np);
771",
772 " if (std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, _nj, .{{}})) |_cp| {{
773",
774 " defer _cp.deinit();
775",
776 " if (_cp.value.object.get(\"choices\")) |_chs|
777",
778 " if (_chs.array.items.len > 0)
779",
780 " if (_chs.array.items[0].object.get(\"delta\")) |_dl|
781",
782 " if (_dl.object.get(\"content\")) |_ct|
783",
784 " if (_ct == .string) try {chunks_var}_content.appendSlice(std.heap.c_allocator, _ct.string);
785",
786 " }} else |_| {{}}
787",
788 " try {chunks_var}.append(std.heap.c_allocator, _nj);
789",
790 " }}"
791 ),
792 chunks_var = chunks_var,
793 stream_var = stream_var,
794 module_name = module_name,
795 stream_next = stream_next,
796 chunk_to_json = chunk_to_json,
797 chunk_free = chunk_free,
798 free_string = free_string,
799 )
800 }
801}
802
803#[derive(Debug, Clone, Copy)]
805enum EventVariant {
806 Page,
807 Error,
808 Complete,
809}
810
811impl EventVariant {
812 fn tag(self) -> &'static str {
814 match self {
815 EventVariant::Page => "page",
816 EventVariant::Error => "error",
817 EventVariant::Complete => "complete",
818 }
819 }
820
821 fn upper_camel(self) -> &'static str {
824 match self {
825 EventVariant::Page => "Page",
826 EventVariant::Error => "Error",
827 EventVariant::Complete => "Complete",
828 }
829 }
830}
831
832fn has_event_variant_accessor(
843 lang: &str,
844 chunks_var: &str,
845 variant: EventVariant,
846 item_type: &str,
847 module_qualifier: Option<&str>,
848) -> Option<String> {
849 let tag = variant.tag();
850 let camel = variant.upper_camel();
851 match lang {
852 "python" => Some(format!("any(e.type == \"{tag}\" for e in {chunks_var})")),
854 "node" | "typescript" => Some(format!("{chunks_var}.some((e: any) => e?.type === \"{tag}\")")),
857 "ruby" => Some(format!("{chunks_var}.any? {{ |e| e.{tag}? }}")),
859 "go" => Some(format!(
863 "func() bool {{ for _, e := range {chunks_var} {{ if _, _ok := e.(pkg.{item_type}{camel}); _ok {{ return true }} }}; return false }}()"
864 )),
865 "java" => Some(format!(
867 "{chunks_var}.stream().anyMatch(e -> e instanceof {item_type}.{camel})"
868 )),
869 "csharp" => module_qualifier.map(|ns| format!("{chunks_var}.Any(e => e is global::{ns}.{item_type}.{camel})")),
872 "swift" => Some(format!(
876 "{chunks_var}.contains(where: {{ e in if case .{tag} = e {{ return true }} else {{ return false }} }})"
877 )),
878 "elixir" => Some(format!(
880 "Enum.any?({chunks_var}, fn e -> Map.get(e, :type) == :{tag} end)"
881 )),
882 "kotlin" => Some(format!("{chunks_var}.any {{ it is {item_type}.{camel} }}")),
884 "kotlin_android" => Some(format!("{chunks_var}.any {{ it is {item_type}.{camel} }}")),
886 "dart" => Some(format!("{chunks_var}.any((e) => e is {item_type}_{camel})")),
888 "zig" => Some(format!(
893 "blk: {{ for ({chunks_var}.items) |_e| {{ if (std.mem.indexOf(u8, _e, \"\\\"type\\\":\\\"{tag}\\\"\") != null) break :blk true; }} break :blk false; }}"
894 )),
895 "rust" => module_qualifier.map(|crate_name| {
899 format!("{chunks_var}.iter().any(|e| matches!(e, {crate_name}::{item_type}::{camel} {{ .. }}))")
900 }),
901 "php" | "wasm" => None,
905 _ => None,
906 }
907}
908
909fn render_swift_tool_calls_deep(root_expr: &str, tail: &str) -> String {
921 use heck::ToLowerCamelCase;
922 let segs = parse_tail(tail);
923 let mut expr = root_expr.to_string();
924 let mut prev_is_optional = false;
932 for seg in &segs {
933 match seg {
934 TailSeg::Index(n) => {
935 expr = format!("({expr})[{n}]");
936 prev_is_optional = false;
937 }
938 TailSeg::Field(f) => {
939 let prop = f.to_lower_camel_case();
940 let sep = if prev_is_optional { "?." } else { "." };
941 expr = format!("{expr}{sep}{prop}");
942 prev_is_optional = true;
946 }
947 }
948 }
949 expr
950}
951
952fn render_rust_tool_calls_deep(chunks_var: &str, tail: &str) -> String {
956 let segs = parse_tail(tail);
957 let idx = segs.iter().find_map(|s| match s {
959 TailSeg::Index(n) => Some(*n),
960 _ => None,
961 });
962 let field_segs: Vec<&str> = segs
963 .iter()
964 .filter_map(|s| match s {
965 TailSeg::Field(f) => Some(f.as_str()),
966 _ => None,
967 })
968 .collect();
969
970 let base = format!(
971 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten()))"
972 );
973 let with_nth = match idx {
974 Some(n) => format!("{base}.nth({n})"),
975 None => base,
976 };
977
978 let mut expr = with_nth;
981 for (i, f) in field_segs.iter().enumerate() {
982 let is_leaf = i == field_segs.len() - 1;
983 if is_leaf {
984 expr = format!("{expr}.and_then(|x| x.{f}.as_deref())");
985 } else {
986 expr = format!("{expr}.and_then(|x| x.{f}.as_ref())");
987 }
988 }
989 format!("{expr}.unwrap_or(\"\")")
990}
991
992#[derive(Debug, PartialEq)]
997enum TailSeg {
998 Index(usize),
999 Field(String),
1000}
1001
1002fn parse_tail(tail: &str) -> Vec<TailSeg> {
1003 let mut segs = Vec::new();
1004 let mut rest = tail;
1005 while !rest.is_empty() {
1006 if let Some(inner) = rest.strip_prefix('[') {
1007 if let Some(close) = inner.find(']') {
1009 let idx_str = &inner[..close];
1010 if let Ok(idx) = idx_str.parse::<usize>() {
1011 segs.push(TailSeg::Index(idx));
1012 }
1013 rest = &inner[close + 1..];
1014 } else {
1015 break;
1016 }
1017 } else if let Some(inner) = rest.strip_prefix('.') {
1018 let end = inner.find(['.', '[']).unwrap_or(inner.len());
1020 segs.push(TailSeg::Field(inner[..end].to_string()));
1021 rest = &inner[end..];
1022 } else {
1023 break;
1024 }
1025 }
1026 segs
1027}
1028
1029fn render_deep_tail(root_expr: &str, tail: &str, lang: &str) -> String {
1032 use heck::{ToLowerCamelCase, ToPascalCase};
1033
1034 let segs = parse_tail(tail);
1035 let mut out = root_expr.to_string();
1036
1037 for seg in &segs {
1038 match (seg, lang) {
1039 (TailSeg::Index(n), "rust") => {
1040 out = format!("({out})[{n}]");
1041 }
1042 (TailSeg::Index(n), "java") => {
1043 out = format!("({out}).get({n})");
1044 }
1045 (TailSeg::Index(n), "kotlin") => {
1046 if *n == 0 {
1047 out = format!("({out}).first()");
1048 } else {
1049 out = format!("({out}).get({n})");
1050 }
1051 }
1052 (TailSeg::Index(n), "kotlin_android") => {
1053 if *n == 0 {
1054 out = format!("({out}).first()");
1055 } else {
1056 out = format!("({out})[{n}]");
1057 }
1058 }
1059 (TailSeg::Index(n), "elixir") => {
1060 out = format!("Enum.at({out}, {n})");
1061 }
1062 (TailSeg::Index(n), "zig") => {
1063 out = format!("({out}).items[{n}]");
1064 }
1065 (TailSeg::Index(n), "php") => {
1066 out = format!("({out})[{n}]");
1067 }
1068 (TailSeg::Index(n), _) => {
1069 out = format!("({out})[{n}]");
1071 }
1072 (TailSeg::Field(f), "rust") => {
1073 use heck::ToSnakeCase;
1074 out.push('.');
1075 out.push_str(&f.to_snake_case());
1076 }
1077 (TailSeg::Field(f), "go") => {
1078 use alef_codegen::naming::to_go_name;
1079 out.push('.');
1080 out.push_str(&to_go_name(f));
1081 }
1082 (TailSeg::Field(f), "java") => {
1083 out.push('.');
1084 out.push_str(&f.to_lower_camel_case());
1085 out.push_str("()");
1086 }
1087 (TailSeg::Field(f), "kotlin") => {
1088 out.push_str("?.");
1094 out.push_str(&f.to_lower_camel_case());
1095 out.push_str("()");
1096 }
1097 (TailSeg::Field(f), "kotlin_android") => {
1098 out.push_str("?.");
1100 out.push_str(&f.to_lower_camel_case());
1101 }
1102 (TailSeg::Field(f), "csharp") => {
1103 out.push('.');
1104 out.push_str(&f.to_pascal_case());
1105 }
1106 (TailSeg::Field(f), "php") => {
1107 out.push_str("->");
1112 out.push_str(f);
1113 }
1114 (TailSeg::Field(f), "elixir") => {
1115 out.push('.');
1116 out.push_str(f);
1117 }
1118 (TailSeg::Field(f), "zig") => {
1119 out.push('.');
1120 out.push_str(f);
1121 }
1122 (TailSeg::Field(f), "python") | (TailSeg::Field(f), "ruby") => {
1123 out.push('.');
1124 out.push_str(f);
1125 }
1126 (TailSeg::Field(f), _) => {
1128 out.push('.');
1129 out.push_str(&f.to_lower_camel_case());
1130 }
1131 }
1132 }
1133
1134 out
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140
1141 #[test]
1142 fn is_streaming_virtual_field_recognizes_all_fields() {
1143 for field in STREAMING_VIRTUAL_FIELDS {
1144 assert!(
1145 is_streaming_virtual_field(field),
1146 "field '{field}' not recognized as streaming virtual"
1147 );
1148 }
1149 }
1150
1151 #[test]
1152 fn is_streaming_virtual_field_rejects_real_fields() {
1153 assert!(!is_streaming_virtual_field("content"));
1154 assert!(!is_streaming_virtual_field("choices"));
1155 assert!(!is_streaming_virtual_field("model"));
1156 assert!(!is_streaming_virtual_field(""));
1157 }
1158
1159 #[test]
1160 fn is_streaming_virtual_field_rejects_non_root_paths_with_matching_tail() {
1161 assert!(!is_streaming_virtual_field("choices[0].finish_reason"));
1166 assert!(!is_streaming_virtual_field("choices[0].message.content"));
1167 assert!(!is_streaming_virtual_field("data[0].embedding"));
1168 }
1169
1170 #[test]
1171 fn is_streaming_virtual_field_does_not_match_usage() {
1172 assert!(!is_streaming_virtual_field("usage"));
1176 assert!(!is_streaming_virtual_field("usage.total_tokens"));
1177 assert!(!is_streaming_virtual_field("usage.prompt_tokens"));
1178 }
1179
1180 #[test]
1181 fn accessor_chunks_returns_var_name() {
1182 assert_eq!(
1183 StreamingFieldResolver::accessor("chunks", "rust", "chunks"),
1184 Some("chunks".to_string())
1185 );
1186 assert_eq!(
1187 StreamingFieldResolver::accessor("chunks", "node", "chunks"),
1188 Some("chunks".to_string())
1189 );
1190 }
1191
1192 #[test]
1193 fn accessor_chunks_length_uses_language_idiom() {
1194 let rust = StreamingFieldResolver::accessor("chunks.length", "rust", "chunks").unwrap();
1195 assert!(rust.contains(".len()"), "rust: {rust}");
1196
1197 let go = StreamingFieldResolver::accessor("chunks.length", "go", "chunks").unwrap();
1198 assert!(go.starts_with("len("), "go: {go}");
1199
1200 let node = StreamingFieldResolver::accessor("chunks.length", "node", "chunks").unwrap();
1201 assert!(node.contains(".length"), "node: {node}");
1202
1203 let php = StreamingFieldResolver::accessor("chunks.length", "php", "chunks").unwrap();
1204 assert!(php.starts_with("count("), "php: {php}");
1205 }
1206
1207 #[test]
1208 fn accessor_chunks_length_zig_uses_items_len() {
1209 let zig = StreamingFieldResolver::accessor("chunks.length", "zig", "chunks").unwrap();
1210 assert_eq!(zig, "chunks.items.len", "zig chunks.length: {zig}");
1211 }
1212
1213 #[test]
1214 fn accessor_stream_content_zig_uses_content_items() {
1215 let zig = StreamingFieldResolver::accessor("stream_content", "zig", "chunks").unwrap();
1216 assert_eq!(zig, "chunks_content.items", "zig stream_content: {zig}");
1217 }
1218
1219 #[test]
1220 fn collect_snippet_zig_drains_via_ffi() {
1221 let snip = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks").unwrap();
1222 assert!(snip.contains("std.ArrayList([]u8)"), "zig collect: {snip}");
1223 assert!(snip.contains("chat_stream_next(_stream_handle)"), "zig collect: {snip}");
1224 assert!(snip.contains("chunks_content"), "zig collect: {snip}");
1225 assert!(
1226 snip.contains("chunks.append(std.heap.c_allocator"),
1227 "zig collect: {snip}"
1228 );
1229 assert!(snip.contains(".empty;"), "zig collect (Zig 0.16 unmanaged): {snip}");
1230 }
1231
1232 #[test]
1233 fn accessor_stream_content_rust_uses_iterator() {
1234 let expr = StreamingFieldResolver::accessor("stream_content", "rust", "chunks").unwrap();
1235 assert!(expr.contains(".collect::<String>()"), "rust stream_content: {expr}");
1236 }
1237
1238 #[test]
1239 fn accessor_no_chunks_after_done_returns_true() {
1240 for lang in ["rust", "go", "java", "php", "node", "wasm", "elixir"] {
1241 let expr = StreamingFieldResolver::accessor("no_chunks_after_done", lang, "chunks").unwrap();
1242 assert_eq!(expr, "true", "lang {lang}: expected 'true', got '{expr}'");
1243 }
1244 }
1245
1246 #[test]
1247 fn accessor_elixir_chunks_length_uses_length_function() {
1248 let expr = StreamingFieldResolver::accessor("chunks.length", "elixir", "chunks").unwrap();
1249 assert_eq!(expr, "length(chunks)", "elixir chunks.length: {expr}");
1250 }
1251
1252 #[test]
1253 fn accessor_elixir_stream_content_uses_pipe() {
1254 let expr = StreamingFieldResolver::accessor("stream_content", "elixir", "chunks").unwrap();
1255 assert!(expr.contains("|> Enum.join"), "elixir stream_content: {expr}");
1256 assert!(expr.contains("|> Enum.map"), "elixir stream_content: {expr}");
1257 assert!(
1259 !expr.contains("choices[0]"),
1260 "elixir stream_content must not use bracket access on list: {expr}"
1261 );
1262 assert!(
1263 expr.contains("Enum.at("),
1264 "elixir stream_content must use Enum.at for list index: {expr}"
1265 );
1266 }
1267
1268 #[test]
1269 fn accessor_elixir_stream_complete_uses_list_last() {
1270 let expr = StreamingFieldResolver::accessor("stream_complete", "elixir", "chunks").unwrap();
1271 assert!(expr.contains("List.last(chunks)"), "elixir stream_complete: {expr}");
1272 assert!(expr.contains("finish_reason != nil"), "elixir stream_complete: {expr}");
1273 assert!(
1275 !expr.contains("choices[0]"),
1276 "elixir stream_complete must not use bracket access on list: {expr}"
1277 );
1278 assert!(
1279 expr.contains("Enum.at("),
1280 "elixir stream_complete must use Enum.at for list index: {expr}"
1281 );
1282 }
1283
1284 #[test]
1285 fn accessor_elixir_finish_reason_uses_list_last() {
1286 let expr = StreamingFieldResolver::accessor("finish_reason", "elixir", "chunks").unwrap();
1287 assert!(expr.contains("List.last(chunks)"), "elixir finish_reason: {expr}");
1288 assert!(expr.contains("finish_reason"), "elixir finish_reason: {expr}");
1289 assert!(
1291 !expr.contains("choices[0]"),
1292 "elixir finish_reason must not use bracket access on list: {expr}"
1293 );
1294 assert!(
1295 expr.contains("Enum.at("),
1296 "elixir finish_reason must use Enum.at for list index: {expr}"
1297 );
1298 }
1299
1300 #[test]
1301 fn collect_snippet_elixir_uses_enum_to_list() {
1302 let snip = StreamingFieldResolver::collect_snippet("elixir", "result", "chunks").unwrap();
1303 assert!(snip.contains("Enum.to_list(result)"), "elixir: {snip}");
1304 assert!(snip.contains("chunks ="), "elixir: {snip}");
1305 }
1306
1307 #[test]
1308 fn collect_snippet_rust_uses_tokio_stream() {
1309 let snip = StreamingFieldResolver::collect_snippet("rust", "result", "chunks").unwrap();
1310 assert!(snip.contains("tokio_stream::StreamExt::collect"), "rust: {snip}");
1311 assert!(snip.contains("let chunks"), "rust: {snip}");
1312 assert!(snip.contains(".expect("), "rust must unwrap Result items: {snip}");
1314 }
1315
1316 #[test]
1317 fn collect_snippet_go_drains_channel() {
1318 let snip = StreamingFieldResolver::collect_snippet("go", "stream", "chunks").unwrap();
1319 assert!(snip.contains("for chunk := range stream"), "go: {snip}");
1320 }
1321
1322 #[test]
1323 fn collect_snippet_java_uses_iterator() {
1324 let snip = StreamingFieldResolver::collect_snippet("java", "result", "chunks").unwrap();
1325 assert!(
1328 snip.contains(".iterator()"),
1329 "java snippet must call .iterator() on stream: {snip}"
1330 );
1331 assert!(snip.contains("hasNext()"), "java: {snip}");
1332 assert!(snip.contains(".next()"), "java: {snip}");
1333 }
1334
1335 #[test]
1336 fn collect_snippet_php_decodes_json_or_iterates() {
1337 let snip = StreamingFieldResolver::collect_snippet("php", "result", "chunks").unwrap();
1338 assert!(snip.contains("json_decode"), "php must decode JSON: {snip}");
1343 assert!(
1344 snip.contains("iterator_to_array"),
1345 "php must keep iterator_to_array fallback: {snip}"
1346 );
1347 assert!(snip.contains("$chunks ="), "php must bind $chunks: {snip}");
1348 }
1349
1350 #[test]
1351 fn collect_snippet_node_uses_for_await() {
1352 let snip = StreamingFieldResolver::collect_snippet("node", "result", "chunks").unwrap();
1353 assert!(snip.contains("for await"), "node: {snip}");
1354 }
1355
1356 #[test]
1357 fn collect_snippet_python_uses_async_for() {
1358 let snip = StreamingFieldResolver::collect_snippet("python", "result", "chunks").unwrap();
1359 assert!(snip.contains("async for chunk in result"), "python: {snip}");
1360 assert!(snip.contains("chunks.append(chunk)"), "python: {snip}");
1361 }
1362
1363 #[test]
1364 fn accessor_stream_content_python_uses_join() {
1365 let expr = StreamingFieldResolver::accessor("stream_content", "python", "chunks").unwrap();
1366 assert!(expr.contains("\"\".join("), "python stream_content: {expr}");
1367 assert!(expr.contains("c.choices"), "python stream_content: {expr}");
1368 }
1369
1370 #[test]
1371 fn accessor_stream_complete_python_uses_finish_reason() {
1372 let expr = StreamingFieldResolver::accessor("stream_complete", "python", "chunks").unwrap();
1373 assert!(
1374 expr.contains("finish_reason is not None"),
1375 "python stream_complete: {expr}"
1376 );
1377 }
1378
1379 #[test]
1380 fn accessor_finish_reason_python_uses_last_chunk() {
1381 let expr = StreamingFieldResolver::accessor("finish_reason", "python", "chunks").unwrap();
1382 assert!(expr.contains("chunks[-1]"), "python finish_reason: {expr}");
1383 assert!(
1385 expr.starts_with("(str(") || expr.contains("str(chunks"),
1386 "python finish_reason must wrap in str(): {expr}"
1387 );
1388 }
1389
1390 #[test]
1391 fn accessor_tool_calls_python_uses_list_comprehension() {
1392 let expr = StreamingFieldResolver::accessor("tool_calls", "python", "chunks").unwrap();
1393 assert!(expr.contains("for c in chunks"), "python tool_calls: {expr}");
1394 assert!(expr.contains("tool_calls"), "python tool_calls: {expr}");
1395 }
1396
1397 #[test]
1398 fn accessor_usage_python_uses_last_chunk() {
1399 let expr = StreamingFieldResolver::accessor("usage", "python", "chunks").unwrap();
1400 assert!(
1401 expr.contains("chunks[-1].usage"),
1402 "python usage: expected chunks[-1].usage, got: {expr}"
1403 );
1404 }
1405
1406 #[test]
1407 fn accessor_usage_total_tokens_does_not_route_via_chunks() {
1408 assert!(StreamingFieldResolver::accessor("usage.total_tokens", "python", "chunks").is_none());
1412 }
1413
1414 #[test]
1415 fn accessor_unknown_field_returns_none() {
1416 assert_eq!(
1417 StreamingFieldResolver::accessor("nonexistent_field", "rust", "chunks"),
1418 None
1419 );
1420 }
1421
1422 #[test]
1427 fn is_streaming_virtual_field_recognizes_deep_tool_calls_paths() {
1428 assert!(
1429 is_streaming_virtual_field("tool_calls[0].function.name"),
1430 "tool_calls[0].function.name should be recognized"
1431 );
1432 assert!(
1433 is_streaming_virtual_field("tool_calls[0].id"),
1434 "tool_calls[0].id should be recognized"
1435 );
1436 assert!(
1437 is_streaming_virtual_field("tool_calls[1].function.arguments"),
1438 "tool_calls[1].function.arguments should be recognized"
1439 );
1440 assert!(is_streaming_virtual_field("tool_calls"));
1442 assert!(!is_streaming_virtual_field("tool_calls_extra.name"));
1444 assert!(!is_streaming_virtual_field("nonexistent[0].field"));
1445 }
1446
1447 #[test]
1454 fn deep_tool_calls_function_name_snapshot_rust_kotlin_ts() {
1455 let field = "tool_calls[0].function.name";
1456
1457 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1458 assert!(
1462 rust.contains(".nth(0)"),
1463 "rust deep tool_calls: expected .nth(0) iterator index, got: {rust}"
1464 );
1465 assert!(
1466 rust.contains("x.function.as_ref()"),
1467 "rust deep tool_calls: expected Option-aware function access, got: {rust}"
1468 );
1469 assert!(
1470 rust.contains("x.name.as_deref()"),
1471 "rust deep tool_calls: expected Option-aware name leaf, got: {rust}"
1472 );
1473 assert!(
1474 !rust.contains("// skipped"),
1475 "rust deep tool_calls: must not emit skip comment, got: {rust}"
1476 );
1477
1478 let kotlin = StreamingFieldResolver::accessor(field, "kotlin", "chunks").unwrap();
1479 assert!(
1481 kotlin.contains(".first()"),
1482 "kotlin deep tool_calls: expected .first() for index 0, got: {kotlin}"
1483 );
1484 assert!(
1485 kotlin.contains(".function()"),
1486 "kotlin deep tool_calls: expected .function() method call, got: {kotlin}"
1487 );
1488 assert!(
1489 kotlin.contains(".name()"),
1490 "kotlin deep tool_calls: expected .name() method call, got: {kotlin}"
1491 );
1492
1493 let ts = StreamingFieldResolver::accessor(field, "node", "chunks").unwrap();
1494 assert!(
1496 ts.contains("[0]"),
1497 "ts/node deep tool_calls: expected [0] index, got: {ts}"
1498 );
1499 assert!(
1500 ts.contains(".function"),
1501 "ts/node deep tool_calls: expected .function segment, got: {ts}"
1502 );
1503 assert!(
1504 ts.contains(".name"),
1505 "ts/node deep tool_calls: expected .name segment, got: {ts}"
1506 );
1507 }
1508
1509 #[test]
1510 fn deep_tool_calls_id_snapshot_all_langs() {
1511 let field = "tool_calls[0].id";
1512
1513 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1514 assert!(rust.contains(".nth(0)"), "rust: {rust}");
1515 assert!(rust.contains("x.id.as_deref()"), "rust: {rust}");
1516
1517 let go = StreamingFieldResolver::accessor(field, "go", "chunks").unwrap();
1518 assert!(go.contains("[0]"), "go: {go}");
1519 assert!(go.contains(".ID"), "go: expected .ID initialism, got: {go}");
1521
1522 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1523 assert!(python.contains("[0]"), "python: {python}");
1524 assert!(python.contains(".id"), "python: {python}");
1525
1526 let php = StreamingFieldResolver::accessor(field, "php", "chunks").unwrap();
1527 assert!(php.contains("[0]"), "php: {php}");
1528 assert!(php.contains("->id"), "php: expected ->id, got: {php}");
1529
1530 let java = StreamingFieldResolver::accessor(field, "java", "chunks").unwrap();
1531 assert!(java.contains(".get(0)"), "java: expected .get(0), got: {java}");
1532 assert!(java.contains(".id()"), "java: expected .id() method call, got: {java}");
1533
1534 let csharp = StreamingFieldResolver::accessor(field, "csharp", "chunks").unwrap();
1535 assert!(csharp.contains("[0]"), "csharp: {csharp}");
1536 assert!(
1537 csharp.contains(".Id"),
1538 "csharp: expected .Id (PascalCase), got: {csharp}"
1539 );
1540
1541 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1542 assert!(elixir.contains("Enum.at("), "elixir: expected Enum.at(, got: {elixir}");
1543 assert!(elixir.contains(".id"), "elixir: {elixir}");
1544 }
1545
1546 #[test]
1547 fn deep_tool_calls_function_name_snapshot_python_elixir_zig() {
1548 let field = "tool_calls[0].function.name";
1549
1550 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1551 assert!(python.contains("[0]"), "python: {python}");
1552 assert!(python.contains(".function"), "python: {python}");
1553 assert!(python.contains(".name"), "python: {python}");
1554
1555 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1556 assert!(elixir.contains("Enum.at("), "elixir: {elixir}");
1558 assert!(elixir.contains(".function"), "elixir: {elixir}");
1559 assert!(elixir.contains(".name"), "elixir: {elixir}");
1560
1561 assert!(
1565 StreamingFieldResolver::accessor(field, "zig", "chunks").is_none(),
1566 "zig: expected None for deep tool_calls path"
1567 );
1568 }
1569
1570 #[test]
1571 fn parse_tail_parses_index_then_field_segments() {
1572 let segs = parse_tail("[0].function.name");
1573 assert_eq!(segs.len(), 3, "expected 3 segments, got: {segs:?}");
1574 assert_eq!(segs[0], TailSeg::Index(0));
1575 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1576 assert_eq!(segs[2], TailSeg::Field("name".to_string()));
1577 }
1578
1579 #[test]
1580 fn parse_tail_parses_simple_index_field() {
1581 let segs = parse_tail("[0].id");
1582 assert_eq!(segs.len(), 2, "expected 2 segments, got: {segs:?}");
1583 assert_eq!(segs[0], TailSeg::Index(0));
1584 assert_eq!(segs[1], TailSeg::Field("id".to_string()));
1585 }
1586
1587 #[test]
1588 fn parse_tail_handles_nonzero_index() {
1589 let segs = parse_tail("[2].function.arguments");
1590 assert_eq!(segs[0], TailSeg::Index(2));
1591 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1592 assert_eq!(segs[2], TailSeg::Field("arguments".to_string()));
1593 }
1594
1595 #[test]
1600 fn accessor_chunks_length_swift_uses_count() {
1601 let swift = StreamingFieldResolver::accessor("chunks.length", "swift", "chunks").unwrap();
1602 assert_eq!(swift, "chunks.count", "swift chunks.length: {swift}");
1603 }
1604
1605 #[test]
1606 fn accessor_stream_content_swift_uses_swift_closures() {
1607 let expr = StreamingFieldResolver::accessor("stream_content", "swift", "chunks").unwrap();
1608 assert!(
1610 expr.contains("{ c in"),
1611 "swift stream_content must use Swift closure syntax, got: {expr}"
1612 );
1613 assert!(
1614 !expr.contains("=>"),
1615 "swift stream_content must not contain JS arrow `=>`, got: {expr}"
1616 );
1617 assert!(
1619 expr.contains("c.choices"),
1620 "swift stream_content must use property access for choices, got: {expr}"
1621 );
1622 assert!(
1623 expr.contains("ch.delta"),
1624 "swift stream_content must use property access for delta, got: {expr}"
1625 );
1626 assert!(
1627 expr.contains("ch.delta.content"),
1628 "swift stream_content must use property access for content, got: {expr}"
1629 );
1630 assert!(
1632 !expr.contains(".toString()"),
1633 "swift stream_content must NOT wrap first-class String fields with .toString(), got: {expr}"
1634 );
1635 assert!(
1636 expr.contains(".joined()"),
1637 "swift stream_content must join with .joined(), got: {expr}"
1638 );
1639 assert!(
1641 !expr.contains(".length"),
1642 "swift stream_content must not use JS .length, got: {expr}"
1643 );
1644 assert!(
1645 !expr.contains(".join("),
1646 "swift stream_content must not use JS .join(, got: {expr}"
1647 );
1648 }
1649
1650 #[test]
1651 fn accessor_stream_complete_swift_uses_swift_syntax() {
1652 let expr = StreamingFieldResolver::accessor("stream_complete", "swift", "chunks").unwrap();
1653 assert!(
1655 expr.contains("isEmpty"),
1656 "swift stream_complete must use .isEmpty, got: {expr}"
1657 );
1658 assert!(
1659 expr.contains(".last!"),
1660 "swift stream_complete must use .last!, got: {expr}"
1661 );
1662 assert!(
1664 expr.contains(".choices.first"),
1665 "swift stream_complete must use property access on choices, got: {expr}"
1666 );
1667 assert!(
1668 expr.contains("finishReason"),
1669 "swift stream_complete must reference lowerCamelCase finishReason, got: {expr}"
1670 );
1671 assert!(
1672 !expr.contains(".length"),
1673 "swift stream_complete must not use JS .length, got: {expr}"
1674 );
1675 assert!(
1676 !expr.contains("!= null"),
1677 "swift stream_complete must not use JS `!= null`, got: {expr}"
1678 );
1679 }
1680
1681 #[test]
1682 fn accessor_tool_calls_swift_uses_swift_flatmap() {
1683 let expr = StreamingFieldResolver::accessor("tool_calls", "swift", "chunks").unwrap();
1684 assert!(
1686 !expr.contains("=>"),
1687 "swift tool_calls must not contain JS arrow `=>`, got: {expr}"
1688 );
1689 assert!(
1690 expr.contains("flatMap"),
1691 "swift tool_calls must use flatMap, got: {expr}"
1692 );
1693 assert!(
1695 expr.contains("c.choices.first"),
1696 "swift tool_calls must use property access on choices, got: {expr}"
1697 );
1698 assert!(
1699 expr.contains("ch.delta.toolCalls"),
1700 "swift tool_calls must use lowerCamelCase toolCalls property, got: {expr}"
1701 );
1702 }
1703
1704 #[test]
1705 fn accessor_tool_calls_deep_path_swift_uses_method_calls_with_optional_chain() {
1706 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "swift", "chunks").unwrap();
1712 assert!(
1713 expr.contains("[0].function"),
1714 "swift deep tool_calls must use plain `.function` directly after array index (non-optional), got: {expr}"
1715 );
1716 assert!(
1717 expr.contains("?.name"),
1718 "swift deep tool_calls must use ?.name property access, got: {expr}"
1719 );
1720 assert!(
1721 !expr.contains(".toString()"),
1722 "swift deep tool_calls must NOT wrap first-class String fields with .toString(), got: {expr}"
1723 );
1724 assert!(
1725 !expr.contains("=>"),
1726 "swift deep tool_calls must not use JS arrow syntax, got: {expr}"
1727 );
1728 }
1729
1730 #[test]
1731 fn accessor_finish_reason_swift_uses_swift_syntax() {
1732 let expr = StreamingFieldResolver::accessor("finish_reason", "swift", "chunks").unwrap();
1733 assert!(
1735 expr.contains("isEmpty"),
1736 "swift finish_reason must use .isEmpty, got: {expr}"
1737 );
1738 assert!(
1739 expr.contains(".last!"),
1740 "swift finish_reason must use .last!, got: {expr}"
1741 );
1742 assert!(
1743 expr.contains("finishReason"),
1744 "swift finish_reason must use lowerCamelCase finishReason property, got: {expr}"
1745 );
1746 assert!(
1748 expr.contains(".rawValue"),
1749 "swift finish_reason must read enum .rawValue, got: {expr}"
1750 );
1751 assert!(
1752 !expr.contains("undefined"),
1753 "swift finish_reason must not use JS `undefined`, got: {expr}"
1754 );
1755 assert!(
1756 !expr.contains(".length"),
1757 "swift finish_reason must not use JS .length, got: {expr}"
1758 );
1759 }
1760
1761 #[test]
1762 fn accessor_usage_swift_uses_swift_syntax() {
1763 let expr = StreamingFieldResolver::accessor("usage", "swift", "chunks").unwrap();
1764 assert!(expr.contains("isEmpty"), "swift usage must use .isEmpty, got: {expr}");
1766 assert!(expr.contains(".last!"), "swift usage must use .last!, got: {expr}");
1767 assert!(
1769 expr.contains(".usage"),
1770 "swift usage must reference .usage property, got: {expr}"
1771 );
1772 assert!(
1773 !expr.contains("usage()"),
1774 "swift usage must NOT use method-call syntax, got: {expr}"
1775 );
1776 assert!(
1777 !expr.contains("undefined"),
1778 "swift usage must not use JS `undefined`, got: {expr}"
1779 );
1780 assert!(
1781 !expr.contains(".length"),
1782 "swift usage must not use JS .length, got: {expr}"
1783 );
1784 }
1785
1786 #[test]
1791 fn kotlin_android_collect_snippet_uses_flow_to_list() {
1792 let snip = StreamingFieldResolver::collect_snippet("kotlin_android", "result", "chunks").unwrap();
1793 assert!(
1795 snip.contains("result.toList()"),
1796 "kotlin_android collect must use Flow.toList(), got: {snip}"
1797 );
1798 assert!(
1799 !snip.contains("asSequence()"),
1800 "kotlin_android collect must NOT use asSequence(), got: {snip}"
1801 );
1802 }
1803
1804 #[test]
1805 fn kotlin_android_stream_content_uses_property_access() {
1806 let expr = StreamingFieldResolver::accessor("stream_content", "kotlin_android", "chunks").unwrap();
1807 assert!(
1808 expr.contains(".choices"),
1809 "kotlin_android stream_content must use .choices property, got: {expr}"
1810 );
1811 assert!(
1812 !expr.contains(".choices()"),
1813 "kotlin_android stream_content must NOT use .choices() getter, got: {expr}"
1814 );
1815 assert!(
1816 expr.contains(".delta"),
1817 "kotlin_android stream_content must use .delta property, got: {expr}"
1818 );
1819 assert!(
1820 !expr.contains(".delta()"),
1821 "kotlin_android stream_content must NOT use .delta() getter, got: {expr}"
1822 );
1823 assert!(
1824 expr.contains(".content"),
1825 "kotlin_android stream_content must use .content property, got: {expr}"
1826 );
1827 assert!(
1828 !expr.contains(".content()"),
1829 "kotlin_android stream_content must NOT use .content() getter, got: {expr}"
1830 );
1831 }
1832
1833 #[test]
1834 fn kotlin_android_finish_reason_uses_name_lowercase_not_get_value() {
1835 let expr = StreamingFieldResolver::accessor("finish_reason", "kotlin_android", "chunks").unwrap();
1836 assert!(
1837 expr.contains(".finishReason"),
1838 "kotlin_android finish_reason must use .finishReason property, got: {expr}"
1839 );
1840 assert!(
1841 !expr.contains(".finishReason()"),
1842 "kotlin_android finish_reason must NOT use .finishReason() getter, got: {expr}"
1843 );
1844 assert!(
1845 expr.contains(".name"),
1846 "kotlin_android finish_reason must use .name for enum wire value, got: {expr}"
1847 );
1848 assert!(
1849 expr.contains(".lowercase()"),
1850 "kotlin_android finish_reason must use .lowercase(), got: {expr}"
1851 );
1852 assert!(
1853 !expr.contains(".getValue()"),
1854 "kotlin_android finish_reason must NOT use .getValue(), got: {expr}"
1855 );
1856 }
1857
1858 #[test]
1859 fn kotlin_android_usage_uses_property_access() {
1860 let expr = StreamingFieldResolver::accessor("usage", "kotlin_android", "chunks").unwrap();
1861 assert!(
1862 expr.contains(".usage"),
1863 "kotlin_android usage must use .usage property, got: {expr}"
1864 );
1865 assert!(
1866 !expr.contains(".usage()"),
1867 "kotlin_android usage must NOT use .usage() getter, got: {expr}"
1868 );
1869 }
1870
1871 #[test]
1872 fn kotlin_android_deep_tool_calls_uses_property_access() {
1873 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "kotlin_android", "chunks").unwrap();
1874 assert!(
1875 expr.contains(".function"),
1876 "kotlin_android deep tool_calls must use .function property, got: {expr}"
1877 );
1878 assert!(
1879 !expr.contains(".function()"),
1880 "kotlin_android deep tool_calls must NOT use .function() getter, got: {expr}"
1881 );
1882 assert!(
1883 expr.contains(".name"),
1884 "kotlin_android deep tool_calls must use .name property, got: {expr}"
1885 );
1886 assert!(
1887 !expr.contains(".name()"),
1888 "kotlin_android deep tool_calls must NOT use .name() getter, got: {expr}"
1889 );
1890 }
1891}