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> {
160 Self::accessor_with_module_qualifier(field, lang, chunks_var, None)
161 }
162
163 pub fn accessor_with_module_qualifier(
166 field: &str,
167 lang: &str,
168 chunks_var: &str,
169 module_qualifier: Option<&str>,
170 ) -> Option<String> {
171 match field {
172 "chunks" => Some(match lang {
173 "zig" => format!("{chunks_var}.items"),
175 "php" => format!("${chunks_var}"),
178 _ => chunks_var.to_string(),
179 }),
180
181 "chunks.length" => Some(match lang {
182 "rust" => format!("{chunks_var}.len()"),
183 "go" => format!("len({chunks_var})"),
184 "python" => format!("len({chunks_var})"),
185 "php" => format!("count(${chunks_var})"),
186 "elixir" => format!("length({chunks_var})"),
187 "kotlin" => format!("{chunks_var}.size"),
189 "zig" => format!("{chunks_var}.items.len"),
191 "swift" => format!("{chunks_var}.count"),
193 _ => format!("{chunks_var}.length"),
195 }),
196
197 "stream_content" => Some(match lang {
198 "rust" => {
199 format!(
200 "{chunks_var}.iter().map(|c| c.choices.first().and_then(|ch| ch.delta.content.as_deref()).unwrap_or(\"\")).collect::<String>()"
201 )
202 }
203 "go" => {
204 format!(
206 "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 }}()"
207 )
208 }
209 "java" => {
210 format!(
211 "{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())"
212 )
213 }
214 "php" => {
215 format!("implode('', array_map(fn($c) => $c->choices[0]->delta->content ?? '', ${chunks_var}))")
216 }
217 "kotlin" => {
218 format!(
221 "{chunks_var}.joinToString(\"\") {{ it.choices()?.firstOrNull()?.delta()?.content() ?: \"\" }}"
222 )
223 }
224 "kotlin_android" => {
225 format!("{chunks_var}.joinToString(\"\") {{ it.choices?.firstOrNull()?.delta?.content ?: \"\" }}")
227 }
228 "elixir" => {
229 format!(
233 "{chunks_var} |> Enum.map(fn c -> (Enum.at(c.choices, 0) || %{{}}) |> Map.get(:delta, %{{}}) |> Map.get(:content, \"\") end) |> Enum.join(\"\")"
234 )
235 }
236 "python" => {
237 format!("\"\".join(c.choices[0].delta.content or \"\" for c in {chunks_var} if c.choices)")
238 }
239 "zig" => {
240 format!("{chunks_var}_content.items")
243 }
244 "swift" => {
250 format!(
251 "{chunks_var}.map {{ c in c.choices.first.flatMap {{ ch in ch.delta.content }} ?? \"\" }}.joined()"
252 )
253 }
254 _ => {
256 format!("{chunks_var}.map((c: any) => c.choices?.[0]?.delta?.content ?? '').join('')")
257 }
258 }),
259
260 "stream_complete" => Some(match lang {
261 "rust" => {
262 format!(
263 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).is_some()"
264 )
265 }
266 "go" => {
267 format!(
268 "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 }}()"
269 )
270 }
271 "java" => {
272 format!(
273 "!{chunks_var}.isEmpty() && {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().flatMap(ch -> java.util.Optional.ofNullable(ch.finishReason())).isPresent()"
274 )
275 }
276 "php" => {
277 format!("!empty(${chunks_var}) && isset(end(${chunks_var})->choices[0]->finishReason)")
281 }
282 "kotlin" => {
283 format!(
285 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices()?.firstOrNull()?.finishReason() != null"
286 )
287 }
288 "kotlin_android" => {
289 format!(
291 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices?.firstOrNull()?.finishReason != null"
292 )
293 }
294 "python" => {
295 format!("bool({chunks_var}) and {chunks_var}[-1].choices[0].finish_reason is not None")
296 }
297 "elixir" => {
298 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason != nil")
299 }
300 "zig" => {
303 format!("{chunks_var}.items.len > 0")
304 }
305 "swift" => {
309 format!("!{chunks_var}.isEmpty && {chunks_var}.last!.choices.first?.finishReason != nil")
310 }
311 _ => {
313 format!(
314 "{chunks_var}.length > 0 && {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason != null"
315 )
316 }
317 }),
318
319 "no_chunks_after_done" => Some(match lang {
323 "rust" => "true".to_string(),
324 "go" => "true".to_string(),
325 "java" => "true".to_string(),
326 "php" => "true".to_string(),
327 _ => "true".to_string(),
328 }),
329
330 "stream.has_page_event" => {
340 has_event_variant_accessor(lang, chunks_var, EventVariant::Page, module_qualifier)
341 }
342 "stream.has_error_event" => {
343 has_event_variant_accessor(lang, chunks_var, EventVariant::Error, module_qualifier)
344 }
345 "stream.has_complete_event" => {
346 has_event_variant_accessor(lang, chunks_var, EventVariant::Complete, module_qualifier)
347 }
348
349 "stream.event_count_min" => Self::accessor("chunks.length", lang, chunks_var),
353
354 "tool_calls" => Some(match lang {
355 "rust" => {
356 format!(
357 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten())).collect::<Vec<_>>()"
358 )
359 }
360 "go" => {
361 format!(
365 "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 }}()"
366 )
367 }
368 "java" => {
369 format!(
370 "{chunks_var}.stream().flatMap(c -> c.choices().stream()).flatMap(ch -> ch.delta().toolCalls() != null ? ch.delta().toolCalls().stream() : java.util.stream.Stream.empty()).toList()"
371 )
372 }
373 "php" => {
374 format!(
377 "array_merge(...array_map(fn($c) => $c->choices[0]->delta->toolCalls ?? [], ${chunks_var}))"
378 )
379 }
380 "kotlin" => {
381 format!(
383 "{chunks_var}.flatMap {{ c -> c.choices()?.flatMap {{ ch -> ch.delta()?.toolCalls() ?: emptyList() }} ?: emptyList() }}"
384 )
385 }
386 "kotlin_android" => {
387 format!(
389 "{chunks_var}.flatMap {{ c -> c.choices?.flatMap {{ ch -> ch.delta?.toolCalls ?: emptyList() }} ?: emptyList() }}"
390 )
391 }
392 "python" => {
393 format!(
394 "[t for c in {chunks_var} for ch in (c.choices or []) for t in (ch.delta.tool_calls or [])]"
395 )
396 }
397 "elixir" => {
398 format!(
399 "{chunks_var} |> Enum.flat_map(fn c -> (List.first(c.choices) || %{{}}).delta |> Map.get(:tool_calls, []) end)"
400 )
401 }
402 "zig" => {
404 format!("{chunks_var}.items")
405 }
406 "swift" => {
410 format!(
411 "{chunks_var}.flatMap {{ c -> [StreamToolCall] in guard let ch = c.choices.first, let tcs = ch.delta.toolCalls else {{ return [] }}; return tcs }}"
412 )
413 }
414 _ => {
415 format!("{chunks_var}.flatMap((c: any) => c.choices?.[0]?.delta?.toolCalls ?? [])")
416 }
417 }),
418
419 "finish_reason" => Some(match lang {
420 "rust" => {
421 format!(
424 "{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()"
425 )
426 }
427 "go" => {
428 format!(
431 "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 \"\" }}()"
432 )
433 }
434 "java" => {
435 format!(
439 "({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))"
440 )
441 }
442 "php" => {
443 format!("(!empty(${chunks_var}) ? (end(${chunks_var})->choices[0]->finishReason ?? null) : null)")
446 }
447 "kotlin" => {
448 format!(
451 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices()?.firstOrNull()?.finishReason()?.getValue())"
452 )
453 }
454 "kotlin_android" => {
455 format!(
457 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices?.firstOrNull()?.finishReason?.name?.lowercase())"
458 )
459 }
460 "python" => {
461 format!(
465 "(str({chunks_var}[-1].choices[0].finish_reason) if {chunks_var} and {chunks_var}[-1].choices else None)"
466 )
467 }
468 "elixir" => {
469 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason")
470 }
471 "zig" => {
474 format!(
475 "(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 \"\"; }})"
476 )
477 }
478 "swift" => {
483 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.choices.first?.finishReason?.rawValue)")
484 }
485 _ => {
486 format!(
487 "{chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason : undefined"
488 )
489 }
490 }),
491
492 "usage" => Some(match lang {
497 "python" => {
498 format!("({chunks_var}[-1].usage if {chunks_var} else None)")
502 }
503 "rust" => {
504 format!("{chunks_var}.last().and_then(|c| c.usage.as_ref())")
505 }
506 "go" => {
507 format!(
508 "func() interface{{}} {{ if len({chunks_var}) == 0 {{ return nil }}; return {chunks_var}[len({chunks_var})-1].Usage }}()"
509 )
510 }
511 "java" => {
512 format!("({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).usage())")
513 }
514 "kotlin" => {
515 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage())")
516 }
517 "kotlin_android" => {
518 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage)")
520 }
521 "php" => {
522 format!("(!empty(${chunks_var}) ? end(${chunks_var})->usage ?? null : null)")
523 }
524 "elixir" => {
525 format!("(if length({chunks_var}) > 0, do: List.last({chunks_var}).usage, else: nil)")
526 }
527 "swift" => {
530 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.usage)")
531 }
532 _ => {
533 format!("({chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].usage : undefined)")
534 }
535 }),
536
537 _ => {
538 if let Some((root, tail)) = split_streaming_deep_path(field) {
542 if lang == "rust" && root == "tool_calls" {
546 return Some(render_rust_tool_calls_deep(chunks_var, tail));
547 }
548 if lang == "swift" && root == "tool_calls" {
552 let root_expr = Self::accessor(root, lang, chunks_var)?;
553 return Some(render_swift_tool_calls_deep(&root_expr, tail));
554 }
555 if lang == "zig" && root == "tool_calls" {
562 return None;
563 }
564 let root_expr = Self::accessor(root, lang, chunks_var)?;
565 Some(render_deep_tail(&root_expr, tail, lang))
566 } else {
567 None
568 }
569 }
570 }
571 }
572
573 pub fn collect_snippet(lang: &str, stream_var: &str, chunks_var: &str) -> Option<String> {
579 match lang {
580 "rust" => Some(format!(
581 "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();"
582 )),
583 "go" => Some(format!(
584 "var {chunks_var} []pkg.ChatCompletionChunk\n\tfor chunk := range {stream_var} {{\n\t\t{chunks_var} = append({chunks_var}, chunk)\n\t}}"
585 )),
586 "java" => Some(format!(
587 "var {chunks_var} = new java.util.ArrayList<ChatCompletionChunk>();\n var _it = {stream_var}.iterator();\n while (_it.hasNext()) {{ {chunks_var}.add(_it.next()); }}"
588 )),
589 "php" => Some(format!(
599 "$__camel = function ($v) use (&$__camel) {{ \
600 if (is_array($v)) {{ \
601 $out = []; \
602 foreach ($v as $k => $vv) {{ \
603 $key = is_string($k) ? lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))) : $k; \
604 $out[$key] = $__camel($vv); \
605 }} \
606 return (array_keys($out) === range(0, count($out) - 1)) ? $out : (object) $out; \
607 }} \
608 if (is_object($v)) {{ \
609 $out = new \\stdClass(); \
610 foreach (get_object_vars($v) as $k => $vv) {{ \
611 $key = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $k)))); \
612 $out->{{$key}} = $__camel($vv); \
613 }} \
614 return $out; \
615 }} \
616 return $v; \
617 }};\n \
618 $__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 \
619 ${chunks_var} = is_string(${stream_var}) \
620 ? array_map($__decode_chunk, (array)(json_decode(${stream_var}, true) ?: [])) \
621 : (is_array(${stream_var}) \
622 ? array_map($__decode_chunk, ${stream_var}) \
623 : array_map($__decode_chunk, iterator_to_array(${stream_var})));"
624 )),
625 "python" => Some(format!(
626 "{chunks_var} = []\n async for chunk in {stream_var}:\n {chunks_var}.append(chunk)"
627 )),
628 "kotlin" => {
629 Some(format!("val {chunks_var} = {stream_var}.asSequence().toList()"))
632 }
633 "kotlin_android" => {
634 Some(format!("val {chunks_var} = {stream_var}.toList()"))
637 }
638 "elixir" => Some(format!("{chunks_var} = Enum.to_list({stream_var})")),
639 "wasm" => Some(format!(
646 "const {chunks_var}: any[] = [];\n while (true) {{ const _chunk = await {stream_var}.next(); if (_chunk == null) break; {chunks_var}.push(_chunk); }}"
647 )),
648 "node" | "typescript" => Some(format!(
649 "const {chunks_var}: any[] = [];\n for await (const _chunk of {stream_var}) {{ {chunks_var}.push(_chunk); }}"
650 )),
651 "swift" => {
652 Some(format!(
657 "var {chunks_var}: [ChatCompletionChunk] = []\n for try await _chunk in {stream_var} {{ {chunks_var}.append(_chunk) }}"
658 ))
659 }
660 "zig" => Some(Self::collect_snippet_zig(stream_var, chunks_var, "module", "ffi")),
661 _ => None,
662 }
663 }
664
665 pub fn collect_snippet_zig(stream_var: &str, chunks_var: &str, module_name: &str, ffi_prefix: &str) -> String {
667 let stream_next = format!("{ffi_prefix}_default_client_chat_stream_next");
668 let chunk_to_json = format!("{ffi_prefix}_chat_completion_chunk_to_json");
669 let chunk_free = format!("{ffi_prefix}_chat_completion_chunk_free");
670 let free_string = format!("{ffi_prefix}_free_string");
671
672 format!(
679 concat!(
680 "var {chunks_var}: std.ArrayList([]u8) = .empty;
681",
682 " defer {{
683",
684 " for ({chunks_var}.items) |_cj| std.heap.c_allocator.free(_cj);
685",
686 " {chunks_var}.deinit(std.heap.c_allocator);
687",
688 " }}
689",
690 " var {chunks_var}_content: std.ArrayList(u8) = .empty;
691",
692 " defer {chunks_var}_content.deinit(std.heap.c_allocator);
693",
694 " while (true) {{
695",
696 " const _nc = {module_name}.c.{stream_next}({stream_var});
697",
698 " if (_nc == null) break;
699",
700 " const _np = {module_name}.c.{chunk_to_json}(_nc);
701",
702 " {module_name}.c.{chunk_free}(_nc);
703",
704 " if (_np == null) continue;
705",
706 " const _ns = std.mem.span(_np);
707",
708 " const _nj = try std.heap.c_allocator.dupe(u8, _ns);
709",
710 " {module_name}.c.{free_string}(_np);
711",
712 " if (std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, _nj, .{{}})) |_cp| {{
713",
714 " defer _cp.deinit();
715",
716 " if (_cp.value.object.get(\"choices\")) |_chs|
717",
718 " if (_chs.array.items.len > 0)
719",
720 " if (_chs.array.items[0].object.get(\"delta\")) |_dl|
721",
722 " if (_dl.object.get(\"content\")) |_ct|
723",
724 " if (_ct == .string) try {chunks_var}_content.appendSlice(std.heap.c_allocator, _ct.string);
725",
726 " }} else |_| {{}}
727",
728 " try {chunks_var}.append(std.heap.c_allocator, _nj);
729",
730 " }}"
731 ),
732 chunks_var = chunks_var,
733 stream_var = stream_var,
734 module_name = module_name,
735 stream_next = stream_next,
736 chunk_to_json = chunk_to_json,
737 chunk_free = chunk_free,
738 free_string = free_string,
739 )
740 }
741}
742
743#[derive(Debug, Clone, Copy)]
745enum EventVariant {
746 Page,
747 Error,
748 Complete,
749}
750
751impl EventVariant {
752 fn tag(self) -> &'static str {
754 match self {
755 EventVariant::Page => "page",
756 EventVariant::Error => "error",
757 EventVariant::Complete => "complete",
758 }
759 }
760
761 fn upper_camel(self) -> &'static str {
764 match self {
765 EventVariant::Page => "Page",
766 EventVariant::Error => "Error",
767 EventVariant::Complete => "Complete",
768 }
769 }
770}
771
772fn has_event_variant_accessor(
778 lang: &str,
779 chunks_var: &str,
780 variant: EventVariant,
781 module_qualifier: Option<&str>,
782) -> Option<String> {
783 let tag = variant.tag();
784 let camel = variant.upper_camel();
785 match lang {
786 "python" => Some(format!("any(e.type == \"{tag}\" for e in {chunks_var})")),
788 "node" | "typescript" => Some(format!("{chunks_var}.some((e: any) => e?.type === \"{tag}\")")),
791 "ruby" => Some(format!("{chunks_var}.any? {{ |e| e.{tag}? }}")),
793 "go" => Some(format!(
797 "func() bool {{ for _, e := range {chunks_var} {{ if _, _ok := e.(pkg.CrawlEvent{camel}); _ok {{ return true }} }}; return false }}()"
798 )),
799 "java" => Some(format!(
801 "{chunks_var}.stream().anyMatch(e -> e instanceof CrawlEvent.{camel})"
802 )),
803 "csharp" => module_qualifier.map(|ns| format!("{chunks_var}.Any(e => e is global::{ns}.CrawlEvent.{camel})")),
806 "swift" => Some(format!(
810 "{chunks_var}.contains(where: {{ e in if case .{tag} = e {{ return true }} else {{ return false }} }})"
811 )),
812 "elixir" => Some(format!(
814 "Enum.any?({chunks_var}, fn e -> Map.get(e, :type) == :{tag} end)"
815 )),
816 "kotlin" => Some(format!("{chunks_var}.any {{ it is CrawlEvent.{camel} }}")),
818 "kotlin_android" => Some(format!("{chunks_var}.any {{ it is CrawlEvent.{camel} }}")),
820 "dart" => Some(format!("{chunks_var}.any((e) => e is CrawlEvent_{camel})")),
823 "zig" => Some(format!(
828 "blk: {{ for ({chunks_var}.items) |_e| {{ if (std.mem.indexOf(u8, _e, \"\\\"type\\\":\\\"{tag}\\\"\") != null) break :blk true; }} break :blk false; }}"
829 )),
830 "rust" => module_qualifier.map(|crate_name| {
837 format!("{chunks_var}.iter().any(|e| matches!(e, {crate_name}::CrawlEvent::{camel} {{ .. }}))")
838 }),
839 "php" | "wasm" => None,
843 _ => None,
844 }
845}
846
847fn render_swift_tool_calls_deep(root_expr: &str, tail: &str) -> String {
859 use heck::ToLowerCamelCase;
860 let segs = parse_tail(tail);
861 let mut expr = root_expr.to_string();
862 let mut prev_is_optional = false;
870 for seg in &segs {
871 match seg {
872 TailSeg::Index(n) => {
873 expr = format!("({expr})[{n}]");
874 prev_is_optional = false;
875 }
876 TailSeg::Field(f) => {
877 let prop = f.to_lower_camel_case();
878 let sep = if prev_is_optional { "?." } else { "." };
879 expr = format!("{expr}{sep}{prop}");
880 prev_is_optional = true;
884 }
885 }
886 }
887 expr
888}
889
890fn render_rust_tool_calls_deep(chunks_var: &str, tail: &str) -> String {
894 let segs = parse_tail(tail);
895 let idx = segs.iter().find_map(|s| match s {
897 TailSeg::Index(n) => Some(*n),
898 _ => None,
899 });
900 let field_segs: Vec<&str> = segs
901 .iter()
902 .filter_map(|s| match s {
903 TailSeg::Field(f) => Some(f.as_str()),
904 _ => None,
905 })
906 .collect();
907
908 let base = format!(
909 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten()))"
910 );
911 let with_nth = match idx {
912 Some(n) => format!("{base}.nth({n})"),
913 None => base,
914 };
915
916 let mut expr = with_nth;
919 for (i, f) in field_segs.iter().enumerate() {
920 let is_leaf = i == field_segs.len() - 1;
921 if is_leaf {
922 expr = format!("{expr}.and_then(|x| x.{f}.as_deref())");
923 } else {
924 expr = format!("{expr}.and_then(|x| x.{f}.as_ref())");
925 }
926 }
927 format!("{expr}.unwrap_or(\"\")")
928}
929
930#[derive(Debug, PartialEq)]
935enum TailSeg {
936 Index(usize),
937 Field(String),
938}
939
940fn parse_tail(tail: &str) -> Vec<TailSeg> {
941 let mut segs = Vec::new();
942 let mut rest = tail;
943 while !rest.is_empty() {
944 if let Some(inner) = rest.strip_prefix('[') {
945 if let Some(close) = inner.find(']') {
947 let idx_str = &inner[..close];
948 if let Ok(idx) = idx_str.parse::<usize>() {
949 segs.push(TailSeg::Index(idx));
950 }
951 rest = &inner[close + 1..];
952 } else {
953 break;
954 }
955 } else if let Some(inner) = rest.strip_prefix('.') {
956 let end = inner.find(['.', '[']).unwrap_or(inner.len());
958 segs.push(TailSeg::Field(inner[..end].to_string()));
959 rest = &inner[end..];
960 } else {
961 break;
962 }
963 }
964 segs
965}
966
967fn render_deep_tail(root_expr: &str, tail: &str, lang: &str) -> String {
970 use heck::{ToLowerCamelCase, ToPascalCase};
971
972 let segs = parse_tail(tail);
973 let mut out = root_expr.to_string();
974
975 for seg in &segs {
976 match (seg, lang) {
977 (TailSeg::Index(n), "rust") => {
978 out = format!("({out})[{n}]");
979 }
980 (TailSeg::Index(n), "java") => {
981 out = format!("({out}).get({n})");
982 }
983 (TailSeg::Index(n), "kotlin") => {
984 if *n == 0 {
985 out = format!("({out}).first()");
986 } else {
987 out = format!("({out}).get({n})");
988 }
989 }
990 (TailSeg::Index(n), "kotlin_android") => {
991 if *n == 0 {
992 out = format!("({out}).first()");
993 } else {
994 out = format!("({out})[{n}]");
995 }
996 }
997 (TailSeg::Index(n), "elixir") => {
998 out = format!("Enum.at({out}, {n})");
999 }
1000 (TailSeg::Index(n), "zig") => {
1001 out = format!("({out}).items[{n}]");
1002 }
1003 (TailSeg::Index(n), "php") => {
1004 out = format!("({out})[{n}]");
1005 }
1006 (TailSeg::Index(n), _) => {
1007 out = format!("({out})[{n}]");
1009 }
1010 (TailSeg::Field(f), "rust") => {
1011 use heck::ToSnakeCase;
1012 out.push('.');
1013 out.push_str(&f.to_snake_case());
1014 }
1015 (TailSeg::Field(f), "go") => {
1016 use alef_codegen::naming::to_go_name;
1017 out.push('.');
1018 out.push_str(&to_go_name(f));
1019 }
1020 (TailSeg::Field(f), "java") => {
1021 out.push('.');
1022 out.push_str(&f.to_lower_camel_case());
1023 out.push_str("()");
1024 }
1025 (TailSeg::Field(f), "kotlin") => {
1026 out.push_str("?.");
1032 out.push_str(&f.to_lower_camel_case());
1033 out.push_str("()");
1034 }
1035 (TailSeg::Field(f), "kotlin_android") => {
1036 out.push_str("?.");
1038 out.push_str(&f.to_lower_camel_case());
1039 }
1040 (TailSeg::Field(f), "csharp") => {
1041 out.push('.');
1042 out.push_str(&f.to_pascal_case());
1043 }
1044 (TailSeg::Field(f), "php") => {
1045 out.push_str("->");
1050 out.push_str(f);
1051 }
1052 (TailSeg::Field(f), "elixir") => {
1053 out.push('.');
1054 out.push_str(f);
1055 }
1056 (TailSeg::Field(f), "zig") => {
1057 out.push('.');
1058 out.push_str(f);
1059 }
1060 (TailSeg::Field(f), "python") | (TailSeg::Field(f), "ruby") => {
1061 out.push('.');
1062 out.push_str(f);
1063 }
1064 (TailSeg::Field(f), _) => {
1066 out.push('.');
1067 out.push_str(&f.to_lower_camel_case());
1068 }
1069 }
1070 }
1071
1072 out
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 use super::*;
1078
1079 #[test]
1080 fn is_streaming_virtual_field_recognizes_all_fields() {
1081 for field in STREAMING_VIRTUAL_FIELDS {
1082 assert!(
1083 is_streaming_virtual_field(field),
1084 "field '{field}' not recognized as streaming virtual"
1085 );
1086 }
1087 }
1088
1089 #[test]
1090 fn is_streaming_virtual_field_rejects_real_fields() {
1091 assert!(!is_streaming_virtual_field("content"));
1092 assert!(!is_streaming_virtual_field("choices"));
1093 assert!(!is_streaming_virtual_field("model"));
1094 assert!(!is_streaming_virtual_field(""));
1095 }
1096
1097 #[test]
1098 fn is_streaming_virtual_field_rejects_non_root_paths_with_matching_tail() {
1099 assert!(!is_streaming_virtual_field("choices[0].finish_reason"));
1104 assert!(!is_streaming_virtual_field("choices[0].message.content"));
1105 assert!(!is_streaming_virtual_field("data[0].embedding"));
1106 }
1107
1108 #[test]
1109 fn is_streaming_virtual_field_does_not_match_usage() {
1110 assert!(!is_streaming_virtual_field("usage"));
1114 assert!(!is_streaming_virtual_field("usage.total_tokens"));
1115 assert!(!is_streaming_virtual_field("usage.prompt_tokens"));
1116 }
1117
1118 #[test]
1119 fn accessor_chunks_returns_var_name() {
1120 assert_eq!(
1121 StreamingFieldResolver::accessor("chunks", "rust", "chunks"),
1122 Some("chunks".to_string())
1123 );
1124 assert_eq!(
1125 StreamingFieldResolver::accessor("chunks", "node", "chunks"),
1126 Some("chunks".to_string())
1127 );
1128 }
1129
1130 #[test]
1131 fn accessor_chunks_length_uses_language_idiom() {
1132 let rust = StreamingFieldResolver::accessor("chunks.length", "rust", "chunks").unwrap();
1133 assert!(rust.contains(".len()"), "rust: {rust}");
1134
1135 let go = StreamingFieldResolver::accessor("chunks.length", "go", "chunks").unwrap();
1136 assert!(go.starts_with("len("), "go: {go}");
1137
1138 let node = StreamingFieldResolver::accessor("chunks.length", "node", "chunks").unwrap();
1139 assert!(node.contains(".length"), "node: {node}");
1140
1141 let php = StreamingFieldResolver::accessor("chunks.length", "php", "chunks").unwrap();
1142 assert!(php.starts_with("count("), "php: {php}");
1143 }
1144
1145 #[test]
1146 fn accessor_chunks_length_zig_uses_items_len() {
1147 let zig = StreamingFieldResolver::accessor("chunks.length", "zig", "chunks").unwrap();
1148 assert_eq!(zig, "chunks.items.len", "zig chunks.length: {zig}");
1149 }
1150
1151 #[test]
1152 fn accessor_stream_content_zig_uses_content_items() {
1153 let zig = StreamingFieldResolver::accessor("stream_content", "zig", "chunks").unwrap();
1154 assert_eq!(zig, "chunks_content.items", "zig stream_content: {zig}");
1155 }
1156
1157 #[test]
1158 fn collect_snippet_zig_drains_via_ffi() {
1159 let snip = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks").unwrap();
1160 assert!(snip.contains("std.ArrayList([]u8)"), "zig collect: {snip}");
1161 assert!(snip.contains("chat_stream_next(_stream_handle)"), "zig collect: {snip}");
1162 assert!(snip.contains("chunks_content"), "zig collect: {snip}");
1163 assert!(
1164 snip.contains("chunks.append(std.heap.c_allocator"),
1165 "zig collect: {snip}"
1166 );
1167 assert!(snip.contains(".empty;"), "zig collect (Zig 0.16 unmanaged): {snip}");
1168 }
1169
1170 #[test]
1171 fn accessor_stream_content_rust_uses_iterator() {
1172 let expr = StreamingFieldResolver::accessor("stream_content", "rust", "chunks").unwrap();
1173 assert!(expr.contains(".collect::<String>()"), "rust stream_content: {expr}");
1174 }
1175
1176 #[test]
1177 fn accessor_no_chunks_after_done_returns_true() {
1178 for lang in ["rust", "go", "java", "php", "node", "wasm", "elixir"] {
1179 let expr = StreamingFieldResolver::accessor("no_chunks_after_done", lang, "chunks").unwrap();
1180 assert_eq!(expr, "true", "lang {lang}: expected 'true', got '{expr}'");
1181 }
1182 }
1183
1184 #[test]
1185 fn accessor_elixir_chunks_length_uses_length_function() {
1186 let expr = StreamingFieldResolver::accessor("chunks.length", "elixir", "chunks").unwrap();
1187 assert_eq!(expr, "length(chunks)", "elixir chunks.length: {expr}");
1188 }
1189
1190 #[test]
1191 fn accessor_elixir_stream_content_uses_pipe() {
1192 let expr = StreamingFieldResolver::accessor("stream_content", "elixir", "chunks").unwrap();
1193 assert!(expr.contains("|> Enum.join"), "elixir stream_content: {expr}");
1194 assert!(expr.contains("|> Enum.map"), "elixir stream_content: {expr}");
1195 assert!(
1197 !expr.contains("choices[0]"),
1198 "elixir stream_content must not use bracket access on list: {expr}"
1199 );
1200 assert!(
1201 expr.contains("Enum.at("),
1202 "elixir stream_content must use Enum.at for list index: {expr}"
1203 );
1204 }
1205
1206 #[test]
1207 fn accessor_elixir_stream_complete_uses_list_last() {
1208 let expr = StreamingFieldResolver::accessor("stream_complete", "elixir", "chunks").unwrap();
1209 assert!(expr.contains("List.last(chunks)"), "elixir stream_complete: {expr}");
1210 assert!(expr.contains("finish_reason != nil"), "elixir stream_complete: {expr}");
1211 assert!(
1213 !expr.contains("choices[0]"),
1214 "elixir stream_complete must not use bracket access on list: {expr}"
1215 );
1216 assert!(
1217 expr.contains("Enum.at("),
1218 "elixir stream_complete must use Enum.at for list index: {expr}"
1219 );
1220 }
1221
1222 #[test]
1223 fn accessor_elixir_finish_reason_uses_list_last() {
1224 let expr = StreamingFieldResolver::accessor("finish_reason", "elixir", "chunks").unwrap();
1225 assert!(expr.contains("List.last(chunks)"), "elixir finish_reason: {expr}");
1226 assert!(expr.contains("finish_reason"), "elixir finish_reason: {expr}");
1227 assert!(
1229 !expr.contains("choices[0]"),
1230 "elixir finish_reason must not use bracket access on list: {expr}"
1231 );
1232 assert!(
1233 expr.contains("Enum.at("),
1234 "elixir finish_reason must use Enum.at for list index: {expr}"
1235 );
1236 }
1237
1238 #[test]
1239 fn collect_snippet_elixir_uses_enum_to_list() {
1240 let snip = StreamingFieldResolver::collect_snippet("elixir", "result", "chunks").unwrap();
1241 assert!(snip.contains("Enum.to_list(result)"), "elixir: {snip}");
1242 assert!(snip.contains("chunks ="), "elixir: {snip}");
1243 }
1244
1245 #[test]
1246 fn collect_snippet_rust_uses_tokio_stream() {
1247 let snip = StreamingFieldResolver::collect_snippet("rust", "result", "chunks").unwrap();
1248 assert!(snip.contains("tokio_stream::StreamExt::collect"), "rust: {snip}");
1249 assert!(snip.contains("let chunks"), "rust: {snip}");
1250 assert!(snip.contains(".expect("), "rust must unwrap Result items: {snip}");
1252 }
1253
1254 #[test]
1255 fn collect_snippet_go_drains_channel() {
1256 let snip = StreamingFieldResolver::collect_snippet("go", "stream", "chunks").unwrap();
1257 assert!(snip.contains("for chunk := range stream"), "go: {snip}");
1258 }
1259
1260 #[test]
1261 fn collect_snippet_java_uses_iterator() {
1262 let snip = StreamingFieldResolver::collect_snippet("java", "result", "chunks").unwrap();
1263 assert!(
1266 snip.contains(".iterator()"),
1267 "java snippet must call .iterator() on stream: {snip}"
1268 );
1269 assert!(snip.contains("hasNext()"), "java: {snip}");
1270 assert!(snip.contains(".next()"), "java: {snip}");
1271 }
1272
1273 #[test]
1274 fn collect_snippet_php_decodes_json_or_iterates() {
1275 let snip = StreamingFieldResolver::collect_snippet("php", "result", "chunks").unwrap();
1276 assert!(snip.contains("json_decode"), "php must decode JSON: {snip}");
1281 assert!(
1282 snip.contains("iterator_to_array"),
1283 "php must keep iterator_to_array fallback: {snip}"
1284 );
1285 assert!(snip.contains("$chunks ="), "php must bind $chunks: {snip}");
1286 }
1287
1288 #[test]
1289 fn collect_snippet_node_uses_for_await() {
1290 let snip = StreamingFieldResolver::collect_snippet("node", "result", "chunks").unwrap();
1291 assert!(snip.contains("for await"), "node: {snip}");
1292 }
1293
1294 #[test]
1295 fn collect_snippet_python_uses_async_for() {
1296 let snip = StreamingFieldResolver::collect_snippet("python", "result", "chunks").unwrap();
1297 assert!(snip.contains("async for chunk in result"), "python: {snip}");
1298 assert!(snip.contains("chunks.append(chunk)"), "python: {snip}");
1299 }
1300
1301 #[test]
1302 fn accessor_stream_content_python_uses_join() {
1303 let expr = StreamingFieldResolver::accessor("stream_content", "python", "chunks").unwrap();
1304 assert!(expr.contains("\"\".join("), "python stream_content: {expr}");
1305 assert!(expr.contains("c.choices"), "python stream_content: {expr}");
1306 }
1307
1308 #[test]
1309 fn accessor_stream_complete_python_uses_finish_reason() {
1310 let expr = StreamingFieldResolver::accessor("stream_complete", "python", "chunks").unwrap();
1311 assert!(
1312 expr.contains("finish_reason is not None"),
1313 "python stream_complete: {expr}"
1314 );
1315 }
1316
1317 #[test]
1318 fn accessor_finish_reason_python_uses_last_chunk() {
1319 let expr = StreamingFieldResolver::accessor("finish_reason", "python", "chunks").unwrap();
1320 assert!(expr.contains("chunks[-1]"), "python finish_reason: {expr}");
1321 assert!(
1323 expr.starts_with("(str(") || expr.contains("str(chunks"),
1324 "python finish_reason must wrap in str(): {expr}"
1325 );
1326 }
1327
1328 #[test]
1329 fn accessor_tool_calls_python_uses_list_comprehension() {
1330 let expr = StreamingFieldResolver::accessor("tool_calls", "python", "chunks").unwrap();
1331 assert!(expr.contains("for c in chunks"), "python tool_calls: {expr}");
1332 assert!(expr.contains("tool_calls"), "python tool_calls: {expr}");
1333 }
1334
1335 #[test]
1336 fn accessor_usage_python_uses_last_chunk() {
1337 let expr = StreamingFieldResolver::accessor("usage", "python", "chunks").unwrap();
1338 assert!(
1339 expr.contains("chunks[-1].usage"),
1340 "python usage: expected chunks[-1].usage, got: {expr}"
1341 );
1342 }
1343
1344 #[test]
1345 fn accessor_usage_total_tokens_does_not_route_via_chunks() {
1346 assert!(StreamingFieldResolver::accessor("usage.total_tokens", "python", "chunks").is_none());
1350 }
1351
1352 #[test]
1353 fn accessor_unknown_field_returns_none() {
1354 assert_eq!(
1355 StreamingFieldResolver::accessor("nonexistent_field", "rust", "chunks"),
1356 None
1357 );
1358 }
1359
1360 #[test]
1365 fn is_streaming_virtual_field_recognizes_deep_tool_calls_paths() {
1366 assert!(
1367 is_streaming_virtual_field("tool_calls[0].function.name"),
1368 "tool_calls[0].function.name should be recognized"
1369 );
1370 assert!(
1371 is_streaming_virtual_field("tool_calls[0].id"),
1372 "tool_calls[0].id should be recognized"
1373 );
1374 assert!(
1375 is_streaming_virtual_field("tool_calls[1].function.arguments"),
1376 "tool_calls[1].function.arguments should be recognized"
1377 );
1378 assert!(is_streaming_virtual_field("tool_calls"));
1380 assert!(!is_streaming_virtual_field("tool_calls_extra.name"));
1382 assert!(!is_streaming_virtual_field("nonexistent[0].field"));
1383 }
1384
1385 #[test]
1392 fn deep_tool_calls_function_name_snapshot_rust_kotlin_ts() {
1393 let field = "tool_calls[0].function.name";
1394
1395 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1396 assert!(
1400 rust.contains(".nth(0)"),
1401 "rust deep tool_calls: expected .nth(0) iterator index, got: {rust}"
1402 );
1403 assert!(
1404 rust.contains("x.function.as_ref()"),
1405 "rust deep tool_calls: expected Option-aware function access, got: {rust}"
1406 );
1407 assert!(
1408 rust.contains("x.name.as_deref()"),
1409 "rust deep tool_calls: expected Option-aware name leaf, got: {rust}"
1410 );
1411 assert!(
1412 !rust.contains("// skipped"),
1413 "rust deep tool_calls: must not emit skip comment, got: {rust}"
1414 );
1415
1416 let kotlin = StreamingFieldResolver::accessor(field, "kotlin", "chunks").unwrap();
1417 assert!(
1419 kotlin.contains(".first()"),
1420 "kotlin deep tool_calls: expected .first() for index 0, got: {kotlin}"
1421 );
1422 assert!(
1423 kotlin.contains(".function()"),
1424 "kotlin deep tool_calls: expected .function() method call, got: {kotlin}"
1425 );
1426 assert!(
1427 kotlin.contains(".name()"),
1428 "kotlin deep tool_calls: expected .name() method call, got: {kotlin}"
1429 );
1430
1431 let ts = StreamingFieldResolver::accessor(field, "node", "chunks").unwrap();
1432 assert!(
1434 ts.contains("[0]"),
1435 "ts/node deep tool_calls: expected [0] index, got: {ts}"
1436 );
1437 assert!(
1438 ts.contains(".function"),
1439 "ts/node deep tool_calls: expected .function segment, got: {ts}"
1440 );
1441 assert!(
1442 ts.contains(".name"),
1443 "ts/node deep tool_calls: expected .name segment, got: {ts}"
1444 );
1445 }
1446
1447 #[test]
1448 fn deep_tool_calls_id_snapshot_all_langs() {
1449 let field = "tool_calls[0].id";
1450
1451 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1452 assert!(rust.contains(".nth(0)"), "rust: {rust}");
1453 assert!(rust.contains("x.id.as_deref()"), "rust: {rust}");
1454
1455 let go = StreamingFieldResolver::accessor(field, "go", "chunks").unwrap();
1456 assert!(go.contains("[0]"), "go: {go}");
1457 assert!(go.contains(".ID"), "go: expected .ID initialism, got: {go}");
1459
1460 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1461 assert!(python.contains("[0]"), "python: {python}");
1462 assert!(python.contains(".id"), "python: {python}");
1463
1464 let php = StreamingFieldResolver::accessor(field, "php", "chunks").unwrap();
1465 assert!(php.contains("[0]"), "php: {php}");
1466 assert!(php.contains("->id"), "php: expected ->id, got: {php}");
1467
1468 let java = StreamingFieldResolver::accessor(field, "java", "chunks").unwrap();
1469 assert!(java.contains(".get(0)"), "java: expected .get(0), got: {java}");
1470 assert!(java.contains(".id()"), "java: expected .id() method call, got: {java}");
1471
1472 let csharp = StreamingFieldResolver::accessor(field, "csharp", "chunks").unwrap();
1473 assert!(csharp.contains("[0]"), "csharp: {csharp}");
1474 assert!(
1475 csharp.contains(".Id"),
1476 "csharp: expected .Id (PascalCase), got: {csharp}"
1477 );
1478
1479 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1480 assert!(elixir.contains("Enum.at("), "elixir: expected Enum.at(, got: {elixir}");
1481 assert!(elixir.contains(".id"), "elixir: {elixir}");
1482 }
1483
1484 #[test]
1485 fn deep_tool_calls_function_name_snapshot_python_elixir_zig() {
1486 let field = "tool_calls[0].function.name";
1487
1488 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1489 assert!(python.contains("[0]"), "python: {python}");
1490 assert!(python.contains(".function"), "python: {python}");
1491 assert!(python.contains(".name"), "python: {python}");
1492
1493 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1494 assert!(elixir.contains("Enum.at("), "elixir: {elixir}");
1496 assert!(elixir.contains(".function"), "elixir: {elixir}");
1497 assert!(elixir.contains(".name"), "elixir: {elixir}");
1498
1499 assert!(
1503 StreamingFieldResolver::accessor(field, "zig", "chunks").is_none(),
1504 "zig: expected None for deep tool_calls path"
1505 );
1506 }
1507
1508 #[test]
1509 fn parse_tail_parses_index_then_field_segments() {
1510 let segs = parse_tail("[0].function.name");
1511 assert_eq!(segs.len(), 3, "expected 3 segments, got: {segs:?}");
1512 assert_eq!(segs[0], TailSeg::Index(0));
1513 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1514 assert_eq!(segs[2], TailSeg::Field("name".to_string()));
1515 }
1516
1517 #[test]
1518 fn parse_tail_parses_simple_index_field() {
1519 let segs = parse_tail("[0].id");
1520 assert_eq!(segs.len(), 2, "expected 2 segments, got: {segs:?}");
1521 assert_eq!(segs[0], TailSeg::Index(0));
1522 assert_eq!(segs[1], TailSeg::Field("id".to_string()));
1523 }
1524
1525 #[test]
1526 fn parse_tail_handles_nonzero_index() {
1527 let segs = parse_tail("[2].function.arguments");
1528 assert_eq!(segs[0], TailSeg::Index(2));
1529 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1530 assert_eq!(segs[2], TailSeg::Field("arguments".to_string()));
1531 }
1532
1533 #[test]
1538 fn accessor_chunks_length_swift_uses_count() {
1539 let swift = StreamingFieldResolver::accessor("chunks.length", "swift", "chunks").unwrap();
1540 assert_eq!(swift, "chunks.count", "swift chunks.length: {swift}");
1541 }
1542
1543 #[test]
1544 fn accessor_stream_content_swift_uses_swift_closures() {
1545 let expr = StreamingFieldResolver::accessor("stream_content", "swift", "chunks").unwrap();
1546 assert!(
1548 expr.contains("{ c in"),
1549 "swift stream_content must use Swift closure syntax, got: {expr}"
1550 );
1551 assert!(
1552 !expr.contains("=>"),
1553 "swift stream_content must not contain JS arrow `=>`, got: {expr}"
1554 );
1555 assert!(
1557 expr.contains("c.choices"),
1558 "swift stream_content must use property access for choices, got: {expr}"
1559 );
1560 assert!(
1561 expr.contains("ch.delta"),
1562 "swift stream_content must use property access for delta, got: {expr}"
1563 );
1564 assert!(
1565 expr.contains("ch.delta.content"),
1566 "swift stream_content must use property access for content, got: {expr}"
1567 );
1568 assert!(
1570 !expr.contains(".toString()"),
1571 "swift stream_content must NOT wrap first-class String fields with .toString(), got: {expr}"
1572 );
1573 assert!(
1574 expr.contains(".joined()"),
1575 "swift stream_content must join with .joined(), got: {expr}"
1576 );
1577 assert!(
1579 !expr.contains(".length"),
1580 "swift stream_content must not use JS .length, got: {expr}"
1581 );
1582 assert!(
1583 !expr.contains(".join("),
1584 "swift stream_content must not use JS .join(, got: {expr}"
1585 );
1586 }
1587
1588 #[test]
1589 fn accessor_stream_complete_swift_uses_swift_syntax() {
1590 let expr = StreamingFieldResolver::accessor("stream_complete", "swift", "chunks").unwrap();
1591 assert!(
1593 expr.contains("isEmpty"),
1594 "swift stream_complete must use .isEmpty, got: {expr}"
1595 );
1596 assert!(
1597 expr.contains(".last!"),
1598 "swift stream_complete must use .last!, got: {expr}"
1599 );
1600 assert!(
1602 expr.contains(".choices.first"),
1603 "swift stream_complete must use property access on choices, got: {expr}"
1604 );
1605 assert!(
1606 expr.contains("finishReason"),
1607 "swift stream_complete must reference lowerCamelCase finishReason, got: {expr}"
1608 );
1609 assert!(
1610 !expr.contains(".length"),
1611 "swift stream_complete must not use JS .length, got: {expr}"
1612 );
1613 assert!(
1614 !expr.contains("!= null"),
1615 "swift stream_complete must not use JS `!= null`, got: {expr}"
1616 );
1617 }
1618
1619 #[test]
1620 fn accessor_tool_calls_swift_uses_swift_flatmap() {
1621 let expr = StreamingFieldResolver::accessor("tool_calls", "swift", "chunks").unwrap();
1622 assert!(
1624 !expr.contains("=>"),
1625 "swift tool_calls must not contain JS arrow `=>`, got: {expr}"
1626 );
1627 assert!(
1628 expr.contains("flatMap"),
1629 "swift tool_calls must use flatMap, got: {expr}"
1630 );
1631 assert!(
1633 expr.contains("c.choices.first"),
1634 "swift tool_calls must use property access on choices, got: {expr}"
1635 );
1636 assert!(
1637 expr.contains("ch.delta.toolCalls"),
1638 "swift tool_calls must use lowerCamelCase toolCalls property, got: {expr}"
1639 );
1640 }
1641
1642 #[test]
1643 fn accessor_tool_calls_deep_path_swift_uses_method_calls_with_optional_chain() {
1644 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "swift", "chunks").unwrap();
1650 assert!(
1651 expr.contains("[0].function"),
1652 "swift deep tool_calls must use plain `.function` directly after array index (non-optional), got: {expr}"
1653 );
1654 assert!(
1655 expr.contains("?.name"),
1656 "swift deep tool_calls must use ?.name property access, got: {expr}"
1657 );
1658 assert!(
1659 !expr.contains(".toString()"),
1660 "swift deep tool_calls must NOT wrap first-class String fields with .toString(), got: {expr}"
1661 );
1662 assert!(
1663 !expr.contains("=>"),
1664 "swift deep tool_calls must not use JS arrow syntax, got: {expr}"
1665 );
1666 }
1667
1668 #[test]
1669 fn accessor_finish_reason_swift_uses_swift_syntax() {
1670 let expr = StreamingFieldResolver::accessor("finish_reason", "swift", "chunks").unwrap();
1671 assert!(
1673 expr.contains("isEmpty"),
1674 "swift finish_reason must use .isEmpty, got: {expr}"
1675 );
1676 assert!(
1677 expr.contains(".last!"),
1678 "swift finish_reason must use .last!, got: {expr}"
1679 );
1680 assert!(
1681 expr.contains("finishReason"),
1682 "swift finish_reason must use lowerCamelCase finishReason property, got: {expr}"
1683 );
1684 assert!(
1686 expr.contains(".rawValue"),
1687 "swift finish_reason must read enum .rawValue, got: {expr}"
1688 );
1689 assert!(
1690 !expr.contains("undefined"),
1691 "swift finish_reason must not use JS `undefined`, got: {expr}"
1692 );
1693 assert!(
1694 !expr.contains(".length"),
1695 "swift finish_reason must not use JS .length, got: {expr}"
1696 );
1697 }
1698
1699 #[test]
1700 fn accessor_usage_swift_uses_swift_syntax() {
1701 let expr = StreamingFieldResolver::accessor("usage", "swift", "chunks").unwrap();
1702 assert!(expr.contains("isEmpty"), "swift usage must use .isEmpty, got: {expr}");
1704 assert!(expr.contains(".last!"), "swift usage must use .last!, got: {expr}");
1705 assert!(
1707 expr.contains(".usage"),
1708 "swift usage must reference .usage property, got: {expr}"
1709 );
1710 assert!(
1711 !expr.contains("usage()"),
1712 "swift usage must NOT use method-call syntax, got: {expr}"
1713 );
1714 assert!(
1715 !expr.contains("undefined"),
1716 "swift usage must not use JS `undefined`, got: {expr}"
1717 );
1718 assert!(
1719 !expr.contains(".length"),
1720 "swift usage must not use JS .length, got: {expr}"
1721 );
1722 }
1723
1724 #[test]
1729 fn kotlin_android_collect_snippet_uses_flow_to_list() {
1730 let snip = StreamingFieldResolver::collect_snippet("kotlin_android", "result", "chunks").unwrap();
1731 assert!(
1733 snip.contains("result.toList()"),
1734 "kotlin_android collect must use Flow.toList(), got: {snip}"
1735 );
1736 assert!(
1737 !snip.contains("asSequence()"),
1738 "kotlin_android collect must NOT use asSequence(), got: {snip}"
1739 );
1740 }
1741
1742 #[test]
1743 fn kotlin_android_stream_content_uses_property_access() {
1744 let expr = StreamingFieldResolver::accessor("stream_content", "kotlin_android", "chunks").unwrap();
1745 assert!(
1746 expr.contains(".choices"),
1747 "kotlin_android stream_content must use .choices property, got: {expr}"
1748 );
1749 assert!(
1750 !expr.contains(".choices()"),
1751 "kotlin_android stream_content must NOT use .choices() getter, got: {expr}"
1752 );
1753 assert!(
1754 expr.contains(".delta"),
1755 "kotlin_android stream_content must use .delta property, got: {expr}"
1756 );
1757 assert!(
1758 !expr.contains(".delta()"),
1759 "kotlin_android stream_content must NOT use .delta() getter, got: {expr}"
1760 );
1761 assert!(
1762 expr.contains(".content"),
1763 "kotlin_android stream_content must use .content property, got: {expr}"
1764 );
1765 assert!(
1766 !expr.contains(".content()"),
1767 "kotlin_android stream_content must NOT use .content() getter, got: {expr}"
1768 );
1769 }
1770
1771 #[test]
1772 fn kotlin_android_finish_reason_uses_name_lowercase_not_get_value() {
1773 let expr = StreamingFieldResolver::accessor("finish_reason", "kotlin_android", "chunks").unwrap();
1774 assert!(
1775 expr.contains(".finishReason"),
1776 "kotlin_android finish_reason must use .finishReason property, got: {expr}"
1777 );
1778 assert!(
1779 !expr.contains(".finishReason()"),
1780 "kotlin_android finish_reason must NOT use .finishReason() getter, got: {expr}"
1781 );
1782 assert!(
1783 expr.contains(".name"),
1784 "kotlin_android finish_reason must use .name for enum wire value, got: {expr}"
1785 );
1786 assert!(
1787 expr.contains(".lowercase()"),
1788 "kotlin_android finish_reason must use .lowercase(), got: {expr}"
1789 );
1790 assert!(
1791 !expr.contains(".getValue()"),
1792 "kotlin_android finish_reason must NOT use .getValue(), got: {expr}"
1793 );
1794 }
1795
1796 #[test]
1797 fn kotlin_android_usage_uses_property_access() {
1798 let expr = StreamingFieldResolver::accessor("usage", "kotlin_android", "chunks").unwrap();
1799 assert!(
1800 expr.contains(".usage"),
1801 "kotlin_android usage must use .usage property, got: {expr}"
1802 );
1803 assert!(
1804 !expr.contains(".usage()"),
1805 "kotlin_android usage must NOT use .usage() getter, got: {expr}"
1806 );
1807 }
1808
1809 #[test]
1810 fn kotlin_android_deep_tool_calls_uses_property_access() {
1811 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "kotlin_android", "chunks").unwrap();
1812 assert!(
1813 expr.contains(".function"),
1814 "kotlin_android deep tool_calls must use .function property, got: {expr}"
1815 );
1816 assert!(
1817 !expr.contains(".function()"),
1818 "kotlin_android deep tool_calls must NOT use .function() getter, got: {expr}"
1819 );
1820 assert!(
1821 expr.contains(".name"),
1822 "kotlin_android deep tool_calls must use .name property, got: {expr}"
1823 );
1824 assert!(
1825 !expr.contains(".name()"),
1826 "kotlin_android deep tool_calls must NOT use .name() getter, got: {expr}"
1827 );
1828 }
1829}