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" => {
232 format!(
233 "{chunks_var}.map {{ c in c.choices.first.flatMap {{ ch in ch.delta.content }} ?? \"\" }}.joined()"
234 )
235 }
236 _ => {
238 format!("{chunks_var}.map((c: any) => c.choices?.[0]?.delta?.content ?? '').join('')")
239 }
240 }),
241
242 "stream_complete" => Some(match lang {
243 "rust" => {
244 format!(
245 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).is_some()"
246 )
247 }
248 "go" => {
249 format!(
250 "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 }}()"
251 )
252 }
253 "java" => {
254 format!(
255 "!{chunks_var}.isEmpty() && {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().flatMap(ch -> java.util.Optional.ofNullable(ch.finishReason())).isPresent()"
256 )
257 }
258 "php" => {
259 format!("!empty(${chunks_var}) && isset(end(${chunks_var})->choices[0]->finishReason)")
263 }
264 "kotlin" => {
265 format!(
267 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices()?.firstOrNull()?.finishReason() != null"
268 )
269 }
270 "kotlin_android" => {
271 format!(
273 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices?.firstOrNull()?.finishReason != null"
274 )
275 }
276 "python" => {
277 format!("bool({chunks_var}) and {chunks_var}[-1].choices[0].finish_reason is not None")
278 }
279 "elixir" => {
280 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason != nil")
281 }
282 "zig" => {
285 format!("{chunks_var}.items.len > 0")
286 }
287 "swift" => {
291 format!("!{chunks_var}.isEmpty && {chunks_var}.last!.choices.first?.finishReason != nil")
292 }
293 _ => {
295 format!(
296 "{chunks_var}.length > 0 && {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason != null"
297 )
298 }
299 }),
300
301 "no_chunks_after_done" => Some(match lang {
305 "rust" => "true".to_string(),
306 "go" => "true".to_string(),
307 "java" => "true".to_string(),
308 "php" => "true".to_string(),
309 _ => "true".to_string(),
310 }),
311
312 "stream.has_page_event" => has_event_variant_accessor(lang, chunks_var, EventVariant::Page),
322 "stream.has_error_event" => has_event_variant_accessor(lang, chunks_var, EventVariant::Error),
323 "stream.has_complete_event" => has_event_variant_accessor(lang, chunks_var, EventVariant::Complete),
324
325 "stream.event_count_min" => Self::accessor("chunks.length", lang, chunks_var),
329
330 "tool_calls" => Some(match lang {
331 "rust" => {
332 format!(
333 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten())).collect::<Vec<_>>()"
334 )
335 }
336 "go" => {
337 format!(
341 "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 }}()"
342 )
343 }
344 "java" => {
345 format!(
346 "{chunks_var}.stream().flatMap(c -> c.choices().stream()).flatMap(ch -> ch.delta().toolCalls() != null ? ch.delta().toolCalls().stream() : java.util.stream.Stream.empty()).toList()"
347 )
348 }
349 "php" => {
350 format!(
353 "array_merge(...array_map(fn($c) => $c->choices[0]->delta->toolCalls ?? [], ${chunks_var}))"
354 )
355 }
356 "kotlin" => {
357 format!(
359 "{chunks_var}.flatMap {{ c -> c.choices()?.flatMap {{ ch -> ch.delta()?.toolCalls() ?: emptyList() }} ?: emptyList() }}"
360 )
361 }
362 "kotlin_android" => {
363 format!(
365 "{chunks_var}.flatMap {{ c -> c.choices?.flatMap {{ ch -> ch.delta?.toolCalls ?: emptyList() }} ?: emptyList() }}"
366 )
367 }
368 "python" => {
369 format!(
370 "[t for c in {chunks_var} for ch in (c.choices or []) for t in (ch.delta.tool_calls or [])]"
371 )
372 }
373 "elixir" => {
374 format!(
375 "{chunks_var} |> Enum.flat_map(fn c -> (List.first(c.choices) || %{{}}).delta |> Map.get(:tool_calls, []) end)"
376 )
377 }
378 "zig" => {
380 format!("{chunks_var}.items")
381 }
382 "swift" => {
386 format!(
387 "{chunks_var}.flatMap {{ c -> [StreamToolCall] in guard let ch = c.choices.first, let tcs = ch.delta.toolCalls else {{ return [] }}; return tcs }}"
388 )
389 }
390 _ => {
391 format!("{chunks_var}.flatMap((c: any) => c.choices?.[0]?.delta?.toolCalls ?? [])")
392 }
393 }),
394
395 "finish_reason" => Some(match lang {
396 "rust" => {
397 format!(
400 "{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()"
401 )
402 }
403 "go" => {
404 format!(
407 "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 \"\" }}()"
408 )
409 }
410 "java" => {
411 format!(
415 "({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))"
416 )
417 }
418 "php" => {
419 format!("(!empty(${chunks_var}) ? (end(${chunks_var})->choices[0]->finishReason ?? null) : null)")
422 }
423 "kotlin" => {
424 format!(
427 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices()?.firstOrNull()?.finishReason()?.getValue())"
428 )
429 }
430 "kotlin_android" => {
431 format!(
433 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices?.firstOrNull()?.finishReason?.name?.lowercase())"
434 )
435 }
436 "python" => {
437 format!(
441 "(str({chunks_var}[-1].choices[0].finish_reason) if {chunks_var} and {chunks_var}[-1].choices else None)"
442 )
443 }
444 "elixir" => {
445 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason")
446 }
447 "zig" => {
450 format!(
451 "(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 \"\"; }})"
452 )
453 }
454 "swift" => {
459 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.choices.first?.finishReason?.rawValue)")
460 }
461 _ => {
462 format!(
463 "{chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason : undefined"
464 )
465 }
466 }),
467
468 "usage" => Some(match lang {
473 "python" => {
474 format!("({chunks_var}[-1].usage if {chunks_var} else None)")
478 }
479 "rust" => {
480 format!("{chunks_var}.last().and_then(|c| c.usage.as_ref())")
481 }
482 "go" => {
483 format!(
484 "func() interface{{}} {{ if len({chunks_var}) == 0 {{ return nil }}; return {chunks_var}[len({chunks_var})-1].Usage }}()"
485 )
486 }
487 "java" => {
488 format!("({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).usage())")
489 }
490 "kotlin" => {
491 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage())")
492 }
493 "kotlin_android" => {
494 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage)")
496 }
497 "php" => {
498 format!("(!empty(${chunks_var}) ? end(${chunks_var})->usage ?? null : null)")
499 }
500 "elixir" => {
501 format!("(if length({chunks_var}) > 0, do: List.last({chunks_var}).usage, else: nil)")
502 }
503 "swift" => {
506 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.usage)")
507 }
508 _ => {
509 format!("({chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].usage : undefined)")
510 }
511 }),
512
513 _ => {
514 if let Some((root, tail)) = split_streaming_deep_path(field) {
518 if lang == "rust" && root == "tool_calls" {
522 return Some(render_rust_tool_calls_deep(chunks_var, tail));
523 }
524 if lang == "swift" && root == "tool_calls" {
528 let root_expr = Self::accessor(root, lang, chunks_var)?;
529 return Some(render_swift_tool_calls_deep(&root_expr, tail));
530 }
531 if lang == "zig" && root == "tool_calls" {
538 return None;
539 }
540 let root_expr = Self::accessor(root, lang, chunks_var)?;
541 Some(render_deep_tail(&root_expr, tail, lang))
542 } else {
543 None
544 }
545 }
546 }
547 }
548
549 pub fn collect_snippet(lang: &str, stream_var: &str, chunks_var: &str) -> Option<String> {
555 match lang {
556 "rust" => Some(format!(
557 "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();"
558 )),
559 "go" => Some(format!(
560 "var {chunks_var} []pkg.ChatCompletionChunk\n\tfor chunk := range {stream_var} {{\n\t\t{chunks_var} = append({chunks_var}, chunk)\n\t}}"
561 )),
562 "java" => Some(format!(
563 "var {chunks_var} = new java.util.ArrayList<ChatCompletionChunk>();\n var _it = {stream_var}.iterator();\n while (_it.hasNext()) {{ {chunks_var}.add(_it.next()); }}"
564 )),
565 "php" => Some(format!(
575 "$__camel = function ($v) use (&$__camel) {{ \
576 if (is_array($v)) {{ \
577 $out = []; \
578 foreach ($v as $k => $vv) {{ \
579 $key = is_string($k) ? lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))) : $k; \
580 $out[$key] = $__camel($vv); \
581 }} \
582 return (array_keys($out) === range(0, count($out) - 1)) ? $out : (object) $out; \
583 }} \
584 if (is_object($v)) {{ \
585 $out = new \\stdClass(); \
586 foreach (get_object_vars($v) as $k => $vv) {{ \
587 $key = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))); \
588 $out->{{$key}} = $__camel($vv); \
589 }} \
590 return $out; \
591 }} \
592 return $v; \
593 }};\n \
594 $__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 \
595 ${chunks_var} = is_string(${stream_var}) \
596 ? array_map($__decode_chunk, (array)(json_decode(${stream_var}, true) ?: [])) \
597 : (is_array(${stream_var}) \
598 ? array_map($__decode_chunk, ${stream_var}) \
599 : array_map($__decode_chunk, iterator_to_array(${stream_var})));"
600 )),
601 "python" => Some(format!(
602 "{chunks_var} = []\n async for chunk in {stream_var}:\n {chunks_var}.append(chunk)"
603 )),
604 "kotlin" => {
605 Some(format!("val {chunks_var} = {stream_var}.asSequence().toList()"))
608 }
609 "kotlin_android" => {
610 Some(format!("val {chunks_var} = {stream_var}.toList()"))
613 }
614 "elixir" => Some(format!("{chunks_var} = Enum.to_list({stream_var})")),
615 "wasm" => Some(format!(
622 "const {chunks_var}: any[] = [];\n while (true) {{ const _chunk = await {stream_var}.next(); if (_chunk == null) break; {chunks_var}.push(_chunk); }}"
623 )),
624 "node" | "typescript" => Some(format!(
625 "const {chunks_var}: any[] = [];\n for await (const _chunk of {stream_var}) {{ {chunks_var}.push(_chunk); }}"
626 )),
627 "swift" => {
628 Some(format!(
633 "var {chunks_var}: [ChatCompletionChunk] = []\n for try await _chunk in {stream_var} {{ {chunks_var}.append(_chunk) }}"
634 ))
635 }
636 "zig" => Some(Self::collect_snippet_zig(stream_var, chunks_var, "module", "ffi")),
637 _ => None,
638 }
639 }
640
641 pub fn collect_snippet_zig(stream_var: &str, chunks_var: &str, module_name: &str, ffi_prefix: &str) -> String {
643 let stream_next = format!("{ffi_prefix}_default_client_chat_stream_next");
644 let chunk_to_json = format!("{ffi_prefix}_chat_completion_chunk_to_json");
645 let chunk_free = format!("{ffi_prefix}_chat_completion_chunk_free");
646 let free_string = format!("{ffi_prefix}_free_string");
647
648 format!(
655 concat!(
656 "var {chunks_var}: std.ArrayList([]u8) = .empty;
657",
658 " defer {{
659",
660 " for ({chunks_var}.items) |_cj| std.heap.c_allocator.free(_cj);
661",
662 " {chunks_var}.deinit(std.heap.c_allocator);
663",
664 " }}
665",
666 " var {chunks_var}_content: std.ArrayList(u8) = .empty;
667",
668 " defer {chunks_var}_content.deinit(std.heap.c_allocator);
669",
670 " while (true) {{
671",
672 " const _nc = {module_name}.c.{stream_next}({stream_var});
673",
674 " if (_nc == null) break;
675",
676 " const _np = {module_name}.c.{chunk_to_json}(_nc);
677",
678 " {module_name}.c.{chunk_free}(_nc);
679",
680 " if (_np == null) continue;
681",
682 " const _ns = std.mem.span(_np);
683",
684 " const _nj = try std.heap.c_allocator.dupe(u8, _ns);
685",
686 " {module_name}.c.{free_string}(_np);
687",
688 " if (std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, _nj, .{{}})) |_cp| {{
689",
690 " defer _cp.deinit();
691",
692 " if (_cp.value.object.get(\"choices\")) |_chs|
693",
694 " if (_chs.array.items.len > 0)
695",
696 " if (_chs.array.items[0].object.get(\"delta\")) |_dl|
697",
698 " if (_dl.object.get(\"content\")) |_ct|
699",
700 " if (_ct == .string) try {chunks_var}_content.appendSlice(std.heap.c_allocator, _ct.string);
701",
702 " }} else |_| {{}}
703",
704 " try {chunks_var}.append(std.heap.c_allocator, _nj);
705",
706 " }}"
707 ),
708 chunks_var = chunks_var,
709 stream_var = stream_var,
710 module_name = module_name,
711 stream_next = stream_next,
712 chunk_to_json = chunk_to_json,
713 chunk_free = chunk_free,
714 free_string = free_string,
715 )
716 }
717}
718
719#[derive(Debug, Clone, Copy)]
721enum EventVariant {
722 Page,
723 Error,
724 Complete,
725}
726
727impl EventVariant {
728 fn tag(self) -> &'static str {
730 match self {
731 EventVariant::Page => "page",
732 EventVariant::Error => "error",
733 EventVariant::Complete => "complete",
734 }
735 }
736
737 fn upper_camel(self) -> &'static str {
740 match self {
741 EventVariant::Page => "Page",
742 EventVariant::Error => "Error",
743 EventVariant::Complete => "Complete",
744 }
745 }
746}
747
748fn has_event_variant_accessor(lang: &str, chunks_var: &str, variant: EventVariant) -> Option<String> {
754 let tag = variant.tag();
755 let camel = variant.upper_camel();
756 match lang {
757 "python" => Some(format!("any(e.type == \"{tag}\" for e in {chunks_var})")),
759 "node" | "typescript" => Some(format!("{chunks_var}.some((e: any) => e?.type === \"{tag}\")")),
762 "ruby" => Some(format!("{chunks_var}.any? {{ |e| e.{tag}? }}")),
764 "go" => Some(format!(
768 "func() bool {{ for _, e := range {chunks_var} {{ if _, _ok := e.(pkg.CrawlEvent{camel}); _ok {{ return true }} }}; return false }}()"
769 )),
770 "java" => Some(format!(
772 "{chunks_var}.stream().anyMatch(e -> e instanceof CrawlEvent.{camel})"
773 )),
774 "csharp" => Some(format!(
776 "{chunks_var}.Any(e => e is global::Kreuzcrawl.CrawlEvent.{camel})"
777 )),
778 "swift" => Some(format!(
782 "{chunks_var}.contains(where: {{ e in if case .{tag} = e {{ return true }} else {{ return false }} }})"
783 )),
784 "elixir" => Some(format!(
786 "Enum.any?({chunks_var}, fn e -> Map.get(e, :type) == :{tag} end)"
787 )),
788 "kotlin" => Some(format!("{chunks_var}.any {{ it is CrawlEvent.{camel} }}")),
790 "kotlin_android" => Some(format!("{chunks_var}.any {{ it is CrawlEvent.{camel} }}")),
792 "dart" => Some(format!("{chunks_var}.any((e) => e is CrawlEvent_{camel})")),
795 "zig" => Some(format!(
800 "blk: {{ for ({chunks_var}.items) |_e| {{ if (std.mem.indexOf(u8, _e, \"\\\"type\\\":\\\"{tag}\\\"\") != null) break :blk true; }} break :blk false; }}"
801 )),
802 "rust" => Some(format!(
807 "{chunks_var}.iter().any(|e| matches!(e, kreuzcrawl::CrawlEvent::{camel} {{ .. }}))"
808 )),
809 "php" | "wasm" => None,
813 _ => None,
814 }
815}
816
817fn render_swift_tool_calls_deep(root_expr: &str, tail: &str) -> String {
829 use heck::ToLowerCamelCase;
830 let segs = parse_tail(tail);
831 let mut expr = root_expr.to_string();
832 let mut prev_is_optional = false;
840 for seg in &segs {
841 match seg {
842 TailSeg::Index(n) => {
843 expr = format!("({expr})[{n}]");
844 prev_is_optional = false;
845 }
846 TailSeg::Field(f) => {
847 let prop = f.to_lower_camel_case();
848 let sep = if prev_is_optional { "?." } else { "." };
849 expr = format!("{expr}{sep}{prop}");
850 prev_is_optional = true;
854 }
855 }
856 }
857 expr
858}
859
860fn render_rust_tool_calls_deep(chunks_var: &str, tail: &str) -> String {
864 let segs = parse_tail(tail);
865 let idx = segs.iter().find_map(|s| match s {
867 TailSeg::Index(n) => Some(*n),
868 _ => None,
869 });
870 let field_segs: Vec<&str> = segs
871 .iter()
872 .filter_map(|s| match s {
873 TailSeg::Field(f) => Some(f.as_str()),
874 _ => None,
875 })
876 .collect();
877
878 let base = format!(
879 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten()))"
880 );
881 let with_nth = match idx {
882 Some(n) => format!("{base}.nth({n})"),
883 None => base,
884 };
885
886 let mut expr = with_nth;
889 for (i, f) in field_segs.iter().enumerate() {
890 let is_leaf = i == field_segs.len() - 1;
891 if is_leaf {
892 expr = format!("{expr}.and_then(|x| x.{f}.as_deref())");
893 } else {
894 expr = format!("{expr}.and_then(|x| x.{f}.as_ref())");
895 }
896 }
897 format!("{expr}.unwrap_or(\"\")")
898}
899
900#[derive(Debug, PartialEq)]
905enum TailSeg {
906 Index(usize),
907 Field(String),
908}
909
910fn parse_tail(tail: &str) -> Vec<TailSeg> {
911 let mut segs = Vec::new();
912 let mut rest = tail;
913 while !rest.is_empty() {
914 if let Some(inner) = rest.strip_prefix('[') {
915 if let Some(close) = inner.find(']') {
917 let idx_str = &inner[..close];
918 if let Ok(idx) = idx_str.parse::<usize>() {
919 segs.push(TailSeg::Index(idx));
920 }
921 rest = &inner[close + 1..];
922 } else {
923 break;
924 }
925 } else if let Some(inner) = rest.strip_prefix('.') {
926 let end = inner.find(['.', '[']).unwrap_or(inner.len());
928 segs.push(TailSeg::Field(inner[..end].to_string()));
929 rest = &inner[end..];
930 } else {
931 break;
932 }
933 }
934 segs
935}
936
937fn render_deep_tail(root_expr: &str, tail: &str, lang: &str) -> String {
940 use heck::{ToLowerCamelCase, ToPascalCase};
941
942 let segs = parse_tail(tail);
943 let mut out = root_expr.to_string();
944
945 for seg in &segs {
946 match (seg, lang) {
947 (TailSeg::Index(n), "rust") => {
948 out = format!("({out})[{n}]");
949 }
950 (TailSeg::Index(n), "java") => {
951 out = format!("({out}).get({n})");
952 }
953 (TailSeg::Index(n), "kotlin") => {
954 if *n == 0 {
955 out = format!("({out}).first()");
956 } else {
957 out = format!("({out}).get({n})");
958 }
959 }
960 (TailSeg::Index(n), "kotlin_android") => {
961 if *n == 0 {
962 out = format!("({out}).first()");
963 } else {
964 out = format!("({out})[{n}]");
965 }
966 }
967 (TailSeg::Index(n), "elixir") => {
968 out = format!("Enum.at({out}, {n})");
969 }
970 (TailSeg::Index(n), "zig") => {
971 out = format!("({out}).items[{n}]");
972 }
973 (TailSeg::Index(n), "php") => {
974 out = format!("({out})[{n}]");
975 }
976 (TailSeg::Index(n), _) => {
977 out = format!("({out})[{n}]");
979 }
980 (TailSeg::Field(f), "rust") => {
981 use heck::ToSnakeCase;
982 out.push('.');
983 out.push_str(&f.to_snake_case());
984 }
985 (TailSeg::Field(f), "go") => {
986 use alef_codegen::naming::to_go_name;
987 out.push('.');
988 out.push_str(&to_go_name(f));
989 }
990 (TailSeg::Field(f), "java") => {
991 out.push('.');
992 out.push_str(&f.to_lower_camel_case());
993 out.push_str("()");
994 }
995 (TailSeg::Field(f), "kotlin") => {
996 out.push_str("?.");
1002 out.push_str(&f.to_lower_camel_case());
1003 out.push_str("()");
1004 }
1005 (TailSeg::Field(f), "kotlin_android") => {
1006 out.push_str("?.");
1008 out.push_str(&f.to_lower_camel_case());
1009 }
1010 (TailSeg::Field(f), "csharp") => {
1011 out.push('.');
1012 out.push_str(&f.to_pascal_case());
1013 }
1014 (TailSeg::Field(f), "php") => {
1015 out.push_str("->");
1020 out.push_str(f);
1021 }
1022 (TailSeg::Field(f), "elixir") => {
1023 out.push('.');
1024 out.push_str(f);
1025 }
1026 (TailSeg::Field(f), "zig") => {
1027 out.push('.');
1028 out.push_str(f);
1029 }
1030 (TailSeg::Field(f), "python") | (TailSeg::Field(f), "ruby") => {
1031 out.push('.');
1032 out.push_str(f);
1033 }
1034 (TailSeg::Field(f), _) => {
1036 out.push('.');
1037 out.push_str(&f.to_lower_camel_case());
1038 }
1039 }
1040 }
1041
1042 out
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048
1049 #[test]
1050 fn is_streaming_virtual_field_recognizes_all_fields() {
1051 for field in STREAMING_VIRTUAL_FIELDS {
1052 assert!(
1053 is_streaming_virtual_field(field),
1054 "field '{field}' not recognized as streaming virtual"
1055 );
1056 }
1057 }
1058
1059 #[test]
1060 fn is_streaming_virtual_field_rejects_real_fields() {
1061 assert!(!is_streaming_virtual_field("content"));
1062 assert!(!is_streaming_virtual_field("choices"));
1063 assert!(!is_streaming_virtual_field("model"));
1064 assert!(!is_streaming_virtual_field(""));
1065 }
1066
1067 #[test]
1068 fn is_streaming_virtual_field_rejects_non_root_paths_with_matching_tail() {
1069 assert!(!is_streaming_virtual_field("choices[0].finish_reason"));
1074 assert!(!is_streaming_virtual_field("choices[0].message.content"));
1075 assert!(!is_streaming_virtual_field("data[0].embedding"));
1076 }
1077
1078 #[test]
1079 fn is_streaming_virtual_field_does_not_match_usage() {
1080 assert!(!is_streaming_virtual_field("usage"));
1084 assert!(!is_streaming_virtual_field("usage.total_tokens"));
1085 assert!(!is_streaming_virtual_field("usage.prompt_tokens"));
1086 }
1087
1088 #[test]
1089 fn accessor_chunks_returns_var_name() {
1090 assert_eq!(
1091 StreamingFieldResolver::accessor("chunks", "rust", "chunks"),
1092 Some("chunks".to_string())
1093 );
1094 assert_eq!(
1095 StreamingFieldResolver::accessor("chunks", "node", "chunks"),
1096 Some("chunks".to_string())
1097 );
1098 }
1099
1100 #[test]
1101 fn accessor_chunks_length_uses_language_idiom() {
1102 let rust = StreamingFieldResolver::accessor("chunks.length", "rust", "chunks").unwrap();
1103 assert!(rust.contains(".len()"), "rust: {rust}");
1104
1105 let go = StreamingFieldResolver::accessor("chunks.length", "go", "chunks").unwrap();
1106 assert!(go.starts_with("len("), "go: {go}");
1107
1108 let node = StreamingFieldResolver::accessor("chunks.length", "node", "chunks").unwrap();
1109 assert!(node.contains(".length"), "node: {node}");
1110
1111 let php = StreamingFieldResolver::accessor("chunks.length", "php", "chunks").unwrap();
1112 assert!(php.starts_with("count("), "php: {php}");
1113 }
1114
1115 #[test]
1116 fn accessor_chunks_length_zig_uses_items_len() {
1117 let zig = StreamingFieldResolver::accessor("chunks.length", "zig", "chunks").unwrap();
1118 assert_eq!(zig, "chunks.items.len", "zig chunks.length: {zig}");
1119 }
1120
1121 #[test]
1122 fn accessor_stream_content_zig_uses_content_items() {
1123 let zig = StreamingFieldResolver::accessor("stream_content", "zig", "chunks").unwrap();
1124 assert_eq!(zig, "chunks_content.items", "zig stream_content: {zig}");
1125 }
1126
1127 #[test]
1128 fn collect_snippet_zig_drains_via_ffi() {
1129 let snip = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks").unwrap();
1130 assert!(snip.contains("std.ArrayList([]u8)"), "zig collect: {snip}");
1131 assert!(snip.contains("chat_stream_next(_stream_handle)"), "zig collect: {snip}");
1132 assert!(snip.contains("chunks_content"), "zig collect: {snip}");
1133 assert!(
1134 snip.contains("chunks.append(std.heap.c_allocator"),
1135 "zig collect: {snip}"
1136 );
1137 assert!(snip.contains(".empty;"), "zig collect (Zig 0.16 unmanaged): {snip}");
1138 }
1139
1140 #[test]
1141 fn accessor_stream_content_rust_uses_iterator() {
1142 let expr = StreamingFieldResolver::accessor("stream_content", "rust", "chunks").unwrap();
1143 assert!(expr.contains(".collect::<String>()"), "rust stream_content: {expr}");
1144 }
1145
1146 #[test]
1147 fn accessor_no_chunks_after_done_returns_true() {
1148 for lang in ["rust", "go", "java", "php", "node", "wasm", "elixir"] {
1149 let expr = StreamingFieldResolver::accessor("no_chunks_after_done", lang, "chunks").unwrap();
1150 assert_eq!(expr, "true", "lang {lang}: expected 'true', got '{expr}'");
1151 }
1152 }
1153
1154 #[test]
1155 fn accessor_elixir_chunks_length_uses_length_function() {
1156 let expr = StreamingFieldResolver::accessor("chunks.length", "elixir", "chunks").unwrap();
1157 assert_eq!(expr, "length(chunks)", "elixir chunks.length: {expr}");
1158 }
1159
1160 #[test]
1161 fn accessor_elixir_stream_content_uses_pipe() {
1162 let expr = StreamingFieldResolver::accessor("stream_content", "elixir", "chunks").unwrap();
1163 assert!(expr.contains("|> Enum.join"), "elixir stream_content: {expr}");
1164 assert!(expr.contains("|> Enum.map"), "elixir stream_content: {expr}");
1165 assert!(
1167 !expr.contains("choices[0]"),
1168 "elixir stream_content must not use bracket access on list: {expr}"
1169 );
1170 assert!(
1171 expr.contains("Enum.at("),
1172 "elixir stream_content must use Enum.at for list index: {expr}"
1173 );
1174 }
1175
1176 #[test]
1177 fn accessor_elixir_stream_complete_uses_list_last() {
1178 let expr = StreamingFieldResolver::accessor("stream_complete", "elixir", "chunks").unwrap();
1179 assert!(expr.contains("List.last(chunks)"), "elixir stream_complete: {expr}");
1180 assert!(expr.contains("finish_reason != nil"), "elixir stream_complete: {expr}");
1181 assert!(
1183 !expr.contains("choices[0]"),
1184 "elixir stream_complete must not use bracket access on list: {expr}"
1185 );
1186 assert!(
1187 expr.contains("Enum.at("),
1188 "elixir stream_complete must use Enum.at for list index: {expr}"
1189 );
1190 }
1191
1192 #[test]
1193 fn accessor_elixir_finish_reason_uses_list_last() {
1194 let expr = StreamingFieldResolver::accessor("finish_reason", "elixir", "chunks").unwrap();
1195 assert!(expr.contains("List.last(chunks)"), "elixir finish_reason: {expr}");
1196 assert!(expr.contains("finish_reason"), "elixir finish_reason: {expr}");
1197 assert!(
1199 !expr.contains("choices[0]"),
1200 "elixir finish_reason must not use bracket access on list: {expr}"
1201 );
1202 assert!(
1203 expr.contains("Enum.at("),
1204 "elixir finish_reason must use Enum.at for list index: {expr}"
1205 );
1206 }
1207
1208 #[test]
1209 fn collect_snippet_elixir_uses_enum_to_list() {
1210 let snip = StreamingFieldResolver::collect_snippet("elixir", "result", "chunks").unwrap();
1211 assert!(snip.contains("Enum.to_list(result)"), "elixir: {snip}");
1212 assert!(snip.contains("chunks ="), "elixir: {snip}");
1213 }
1214
1215 #[test]
1216 fn collect_snippet_rust_uses_tokio_stream() {
1217 let snip = StreamingFieldResolver::collect_snippet("rust", "result", "chunks").unwrap();
1218 assert!(snip.contains("tokio_stream::StreamExt::collect"), "rust: {snip}");
1219 assert!(snip.contains("let chunks"), "rust: {snip}");
1220 assert!(snip.contains(".expect("), "rust must unwrap Result items: {snip}");
1222 }
1223
1224 #[test]
1225 fn collect_snippet_go_drains_channel() {
1226 let snip = StreamingFieldResolver::collect_snippet("go", "stream", "chunks").unwrap();
1227 assert!(snip.contains("for chunk := range stream"), "go: {snip}");
1228 }
1229
1230 #[test]
1231 fn collect_snippet_java_uses_iterator() {
1232 let snip = StreamingFieldResolver::collect_snippet("java", "result", "chunks").unwrap();
1233 assert!(
1236 snip.contains(".iterator()"),
1237 "java snippet must call .iterator() on stream: {snip}"
1238 );
1239 assert!(snip.contains("hasNext()"), "java: {snip}");
1240 assert!(snip.contains(".next()"), "java: {snip}");
1241 }
1242
1243 #[test]
1244 fn collect_snippet_php_decodes_json_or_iterates() {
1245 let snip = StreamingFieldResolver::collect_snippet("php", "result", "chunks").unwrap();
1246 assert!(snip.contains("json_decode"), "php must decode JSON: {snip}");
1251 assert!(
1252 snip.contains("iterator_to_array"),
1253 "php must keep iterator_to_array fallback: {snip}"
1254 );
1255 assert!(snip.contains("$chunks ="), "php must bind $chunks: {snip}");
1256 }
1257
1258 #[test]
1259 fn collect_snippet_node_uses_for_await() {
1260 let snip = StreamingFieldResolver::collect_snippet("node", "result", "chunks").unwrap();
1261 assert!(snip.contains("for await"), "node: {snip}");
1262 }
1263
1264 #[test]
1265 fn collect_snippet_python_uses_async_for() {
1266 let snip = StreamingFieldResolver::collect_snippet("python", "result", "chunks").unwrap();
1267 assert!(snip.contains("async for chunk in result"), "python: {snip}");
1268 assert!(snip.contains("chunks.append(chunk)"), "python: {snip}");
1269 }
1270
1271 #[test]
1272 fn accessor_stream_content_python_uses_join() {
1273 let expr = StreamingFieldResolver::accessor("stream_content", "python", "chunks").unwrap();
1274 assert!(expr.contains("\"\".join("), "python stream_content: {expr}");
1275 assert!(expr.contains("c.choices"), "python stream_content: {expr}");
1276 }
1277
1278 #[test]
1279 fn accessor_stream_complete_python_uses_finish_reason() {
1280 let expr = StreamingFieldResolver::accessor("stream_complete", "python", "chunks").unwrap();
1281 assert!(
1282 expr.contains("finish_reason is not None"),
1283 "python stream_complete: {expr}"
1284 );
1285 }
1286
1287 #[test]
1288 fn accessor_finish_reason_python_uses_last_chunk() {
1289 let expr = StreamingFieldResolver::accessor("finish_reason", "python", "chunks").unwrap();
1290 assert!(expr.contains("chunks[-1]"), "python finish_reason: {expr}");
1291 assert!(
1293 expr.starts_with("(str(") || expr.contains("str(chunks"),
1294 "python finish_reason must wrap in str(): {expr}"
1295 );
1296 }
1297
1298 #[test]
1299 fn accessor_tool_calls_python_uses_list_comprehension() {
1300 let expr = StreamingFieldResolver::accessor("tool_calls", "python", "chunks").unwrap();
1301 assert!(expr.contains("for c in chunks"), "python tool_calls: {expr}");
1302 assert!(expr.contains("tool_calls"), "python tool_calls: {expr}");
1303 }
1304
1305 #[test]
1306 fn accessor_usage_python_uses_last_chunk() {
1307 let expr = StreamingFieldResolver::accessor("usage", "python", "chunks").unwrap();
1308 assert!(
1309 expr.contains("chunks[-1].usage"),
1310 "python usage: expected chunks[-1].usage, got: {expr}"
1311 );
1312 }
1313
1314 #[test]
1315 fn accessor_usage_total_tokens_does_not_route_via_chunks() {
1316 assert!(StreamingFieldResolver::accessor("usage.total_tokens", "python", "chunks").is_none());
1320 }
1321
1322 #[test]
1323 fn accessor_unknown_field_returns_none() {
1324 assert_eq!(
1325 StreamingFieldResolver::accessor("nonexistent_field", "rust", "chunks"),
1326 None
1327 );
1328 }
1329
1330 #[test]
1335 fn is_streaming_virtual_field_recognizes_deep_tool_calls_paths() {
1336 assert!(
1337 is_streaming_virtual_field("tool_calls[0].function.name"),
1338 "tool_calls[0].function.name should be recognized"
1339 );
1340 assert!(
1341 is_streaming_virtual_field("tool_calls[0].id"),
1342 "tool_calls[0].id should be recognized"
1343 );
1344 assert!(
1345 is_streaming_virtual_field("tool_calls[1].function.arguments"),
1346 "tool_calls[1].function.arguments should be recognized"
1347 );
1348 assert!(is_streaming_virtual_field("tool_calls"));
1350 assert!(!is_streaming_virtual_field("tool_calls_extra.name"));
1352 assert!(!is_streaming_virtual_field("nonexistent[0].field"));
1353 }
1354
1355 #[test]
1362 fn deep_tool_calls_function_name_snapshot_rust_kotlin_ts() {
1363 let field = "tool_calls[0].function.name";
1364
1365 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1366 assert!(
1370 rust.contains(".nth(0)"),
1371 "rust deep tool_calls: expected .nth(0) iterator index, got: {rust}"
1372 );
1373 assert!(
1374 rust.contains("x.function.as_ref()"),
1375 "rust deep tool_calls: expected Option-aware function access, got: {rust}"
1376 );
1377 assert!(
1378 rust.contains("x.name.as_deref()"),
1379 "rust deep tool_calls: expected Option-aware name leaf, got: {rust}"
1380 );
1381 assert!(
1382 !rust.contains("// skipped"),
1383 "rust deep tool_calls: must not emit skip comment, got: {rust}"
1384 );
1385
1386 let kotlin = StreamingFieldResolver::accessor(field, "kotlin", "chunks").unwrap();
1387 assert!(
1389 kotlin.contains(".first()"),
1390 "kotlin deep tool_calls: expected .first() for index 0, got: {kotlin}"
1391 );
1392 assert!(
1393 kotlin.contains(".function()"),
1394 "kotlin deep tool_calls: expected .function() method call, got: {kotlin}"
1395 );
1396 assert!(
1397 kotlin.contains(".name()"),
1398 "kotlin deep tool_calls: expected .name() method call, got: {kotlin}"
1399 );
1400
1401 let ts = StreamingFieldResolver::accessor(field, "node", "chunks").unwrap();
1402 assert!(
1404 ts.contains("[0]"),
1405 "ts/node deep tool_calls: expected [0] index, got: {ts}"
1406 );
1407 assert!(
1408 ts.contains(".function"),
1409 "ts/node deep tool_calls: expected .function segment, got: {ts}"
1410 );
1411 assert!(
1412 ts.contains(".name"),
1413 "ts/node deep tool_calls: expected .name segment, got: {ts}"
1414 );
1415 }
1416
1417 #[test]
1418 fn deep_tool_calls_id_snapshot_all_langs() {
1419 let field = "tool_calls[0].id";
1420
1421 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1422 assert!(rust.contains(".nth(0)"), "rust: {rust}");
1423 assert!(rust.contains("x.id.as_deref()"), "rust: {rust}");
1424
1425 let go = StreamingFieldResolver::accessor(field, "go", "chunks").unwrap();
1426 assert!(go.contains("[0]"), "go: {go}");
1427 assert!(go.contains(".ID"), "go: expected .ID initialism, got: {go}");
1429
1430 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1431 assert!(python.contains("[0]"), "python: {python}");
1432 assert!(python.contains(".id"), "python: {python}");
1433
1434 let php = StreamingFieldResolver::accessor(field, "php", "chunks").unwrap();
1435 assert!(php.contains("[0]"), "php: {php}");
1436 assert!(php.contains("->id"), "php: expected ->id, got: {php}");
1437
1438 let java = StreamingFieldResolver::accessor(field, "java", "chunks").unwrap();
1439 assert!(java.contains(".get(0)"), "java: expected .get(0), got: {java}");
1440 assert!(java.contains(".id()"), "java: expected .id() method call, got: {java}");
1441
1442 let csharp = StreamingFieldResolver::accessor(field, "csharp", "chunks").unwrap();
1443 assert!(csharp.contains("[0]"), "csharp: {csharp}");
1444 assert!(
1445 csharp.contains(".Id"),
1446 "csharp: expected .Id (PascalCase), got: {csharp}"
1447 );
1448
1449 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1450 assert!(elixir.contains("Enum.at("), "elixir: expected Enum.at(, got: {elixir}");
1451 assert!(elixir.contains(".id"), "elixir: {elixir}");
1452 }
1453
1454 #[test]
1455 fn deep_tool_calls_function_name_snapshot_python_elixir_zig() {
1456 let field = "tool_calls[0].function.name";
1457
1458 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1459 assert!(python.contains("[0]"), "python: {python}");
1460 assert!(python.contains(".function"), "python: {python}");
1461 assert!(python.contains(".name"), "python: {python}");
1462
1463 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1464 assert!(elixir.contains("Enum.at("), "elixir: {elixir}");
1466 assert!(elixir.contains(".function"), "elixir: {elixir}");
1467 assert!(elixir.contains(".name"), "elixir: {elixir}");
1468
1469 assert!(
1473 StreamingFieldResolver::accessor(field, "zig", "chunks").is_none(),
1474 "zig: expected None for deep tool_calls path"
1475 );
1476 }
1477
1478 #[test]
1479 fn parse_tail_parses_index_then_field_segments() {
1480 let segs = parse_tail("[0].function.name");
1481 assert_eq!(segs.len(), 3, "expected 3 segments, got: {segs:?}");
1482 assert_eq!(segs[0], TailSeg::Index(0));
1483 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1484 assert_eq!(segs[2], TailSeg::Field("name".to_string()));
1485 }
1486
1487 #[test]
1488 fn parse_tail_parses_simple_index_field() {
1489 let segs = parse_tail("[0].id");
1490 assert_eq!(segs.len(), 2, "expected 2 segments, got: {segs:?}");
1491 assert_eq!(segs[0], TailSeg::Index(0));
1492 assert_eq!(segs[1], TailSeg::Field("id".to_string()));
1493 }
1494
1495 #[test]
1496 fn parse_tail_handles_nonzero_index() {
1497 let segs = parse_tail("[2].function.arguments");
1498 assert_eq!(segs[0], TailSeg::Index(2));
1499 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1500 assert_eq!(segs[2], TailSeg::Field("arguments".to_string()));
1501 }
1502
1503 #[test]
1508 fn accessor_chunks_length_swift_uses_count() {
1509 let swift = StreamingFieldResolver::accessor("chunks.length", "swift", "chunks").unwrap();
1510 assert_eq!(swift, "chunks.count", "swift chunks.length: {swift}");
1511 }
1512
1513 #[test]
1514 fn accessor_stream_content_swift_uses_swift_closures() {
1515 let expr = StreamingFieldResolver::accessor("stream_content", "swift", "chunks").unwrap();
1516 assert!(
1518 expr.contains("{ c in"),
1519 "swift stream_content must use Swift closure syntax, got: {expr}"
1520 );
1521 assert!(
1522 !expr.contains("=>"),
1523 "swift stream_content must not contain JS arrow `=>`, got: {expr}"
1524 );
1525 assert!(
1527 expr.contains("c.choices"),
1528 "swift stream_content must use property access for choices, got: {expr}"
1529 );
1530 assert!(
1531 expr.contains("ch.delta"),
1532 "swift stream_content must use property access for delta, got: {expr}"
1533 );
1534 assert!(
1535 expr.contains("ch.delta.content"),
1536 "swift stream_content must use property access for content, got: {expr}"
1537 );
1538 assert!(
1540 !expr.contains(".toString()"),
1541 "swift stream_content must NOT wrap first-class String fields with .toString(), got: {expr}"
1542 );
1543 assert!(
1544 expr.contains(".joined()"),
1545 "swift stream_content must join with .joined(), got: {expr}"
1546 );
1547 assert!(
1549 !expr.contains(".length"),
1550 "swift stream_content must not use JS .length, got: {expr}"
1551 );
1552 assert!(
1553 !expr.contains(".join("),
1554 "swift stream_content must not use JS .join(, got: {expr}"
1555 );
1556 }
1557
1558 #[test]
1559 fn accessor_stream_complete_swift_uses_swift_syntax() {
1560 let expr = StreamingFieldResolver::accessor("stream_complete", "swift", "chunks").unwrap();
1561 assert!(
1563 expr.contains("isEmpty"),
1564 "swift stream_complete must use .isEmpty, got: {expr}"
1565 );
1566 assert!(
1567 expr.contains(".last!"),
1568 "swift stream_complete must use .last!, got: {expr}"
1569 );
1570 assert!(
1572 expr.contains(".choices.first"),
1573 "swift stream_complete must use property access on choices, got: {expr}"
1574 );
1575 assert!(
1576 expr.contains("finishReason"),
1577 "swift stream_complete must reference lowerCamelCase finishReason, 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!(
1603 expr.contains("c.choices.first"),
1604 "swift tool_calls must use property access on choices, got: {expr}"
1605 );
1606 assert!(
1607 expr.contains("ch.delta.toolCalls"),
1608 "swift tool_calls must use lowerCamelCase toolCalls property, got: {expr}"
1609 );
1610 }
1611
1612 #[test]
1613 fn accessor_tool_calls_deep_path_swift_uses_method_calls_with_optional_chain() {
1614 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "swift", "chunks").unwrap();
1620 assert!(
1621 expr.contains("[0].function"),
1622 "swift deep tool_calls must use plain `.function` directly after array index (non-optional), got: {expr}"
1623 );
1624 assert!(
1625 expr.contains("?.name"),
1626 "swift deep tool_calls must use ?.name property access, got: {expr}"
1627 );
1628 assert!(
1629 !expr.contains(".toString()"),
1630 "swift deep tool_calls must NOT wrap first-class String fields with .toString(), got: {expr}"
1631 );
1632 assert!(
1633 !expr.contains("=>"),
1634 "swift deep tool_calls must not use JS arrow syntax, got: {expr}"
1635 );
1636 }
1637
1638 #[test]
1639 fn accessor_finish_reason_swift_uses_swift_syntax() {
1640 let expr = StreamingFieldResolver::accessor("finish_reason", "swift", "chunks").unwrap();
1641 assert!(
1643 expr.contains("isEmpty"),
1644 "swift finish_reason must use .isEmpty, got: {expr}"
1645 );
1646 assert!(
1647 expr.contains(".last!"),
1648 "swift finish_reason must use .last!, got: {expr}"
1649 );
1650 assert!(
1651 expr.contains("finishReason"),
1652 "swift finish_reason must use lowerCamelCase finishReason property, got: {expr}"
1653 );
1654 assert!(
1656 expr.contains(".rawValue"),
1657 "swift finish_reason must read enum .rawValue, 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!(
1677 expr.contains(".usage"),
1678 "swift usage must reference .usage property, got: {expr}"
1679 );
1680 assert!(
1681 !expr.contains("usage()"),
1682 "swift usage must NOT use method-call syntax, got: {expr}"
1683 );
1684 assert!(
1685 !expr.contains("undefined"),
1686 "swift usage must not use JS `undefined`, got: {expr}"
1687 );
1688 assert!(
1689 !expr.contains(".length"),
1690 "swift usage must not use JS .length, got: {expr}"
1691 );
1692 }
1693
1694 #[test]
1699 fn kotlin_android_collect_snippet_uses_flow_to_list() {
1700 let snip = StreamingFieldResolver::collect_snippet("kotlin_android", "result", "chunks").unwrap();
1701 assert!(
1703 snip.contains("result.toList()"),
1704 "kotlin_android collect must use Flow.toList(), got: {snip}"
1705 );
1706 assert!(
1707 !snip.contains("asSequence()"),
1708 "kotlin_android collect must NOT use asSequence(), got: {snip}"
1709 );
1710 }
1711
1712 #[test]
1713 fn kotlin_android_stream_content_uses_property_access() {
1714 let expr = StreamingFieldResolver::accessor("stream_content", "kotlin_android", "chunks").unwrap();
1715 assert!(
1716 expr.contains(".choices"),
1717 "kotlin_android stream_content must use .choices property, got: {expr}"
1718 );
1719 assert!(
1720 !expr.contains(".choices()"),
1721 "kotlin_android stream_content must NOT use .choices() getter, got: {expr}"
1722 );
1723 assert!(
1724 expr.contains(".delta"),
1725 "kotlin_android stream_content must use .delta property, got: {expr}"
1726 );
1727 assert!(
1728 !expr.contains(".delta()"),
1729 "kotlin_android stream_content must NOT use .delta() getter, got: {expr}"
1730 );
1731 assert!(
1732 expr.contains(".content"),
1733 "kotlin_android stream_content must use .content property, got: {expr}"
1734 );
1735 assert!(
1736 !expr.contains(".content()"),
1737 "kotlin_android stream_content must NOT use .content() getter, got: {expr}"
1738 );
1739 }
1740
1741 #[test]
1742 fn kotlin_android_finish_reason_uses_name_lowercase_not_get_value() {
1743 let expr = StreamingFieldResolver::accessor("finish_reason", "kotlin_android", "chunks").unwrap();
1744 assert!(
1745 expr.contains(".finishReason"),
1746 "kotlin_android finish_reason must use .finishReason property, got: {expr}"
1747 );
1748 assert!(
1749 !expr.contains(".finishReason()"),
1750 "kotlin_android finish_reason must NOT use .finishReason() getter, got: {expr}"
1751 );
1752 assert!(
1753 expr.contains(".name"),
1754 "kotlin_android finish_reason must use .name for enum wire value, got: {expr}"
1755 );
1756 assert!(
1757 expr.contains(".lowercase()"),
1758 "kotlin_android finish_reason must use .lowercase(), got: {expr}"
1759 );
1760 assert!(
1761 !expr.contains(".getValue()"),
1762 "kotlin_android finish_reason must NOT use .getValue(), got: {expr}"
1763 );
1764 }
1765
1766 #[test]
1767 fn kotlin_android_usage_uses_property_access() {
1768 let expr = StreamingFieldResolver::accessor("usage", "kotlin_android", "chunks").unwrap();
1769 assert!(
1770 expr.contains(".usage"),
1771 "kotlin_android usage must use .usage property, got: {expr}"
1772 );
1773 assert!(
1774 !expr.contains(".usage()"),
1775 "kotlin_android usage must NOT use .usage() getter, got: {expr}"
1776 );
1777 }
1778
1779 #[test]
1780 fn kotlin_android_deep_tool_calls_uses_property_access() {
1781 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "kotlin_android", "chunks").unwrap();
1782 assert!(
1783 expr.contains(".function"),
1784 "kotlin_android deep tool_calls must use .function property, got: {expr}"
1785 );
1786 assert!(
1787 !expr.contains(".function()"),
1788 "kotlin_android deep tool_calls must NOT use .function() getter, got: {expr}"
1789 );
1790 assert!(
1791 expr.contains(".name"),
1792 "kotlin_android deep tool_calls must use .name property, got: {expr}"
1793 );
1794 assert!(
1795 !expr.contains(".name()"),
1796 "kotlin_android deep tool_calls must NOT use .name() getter, got: {expr}"
1797 );
1798 }
1799}