1use crate::config::E2eConfig;
16use crate::escape::{escape_java as escape_swift_str, expand_fixture_templates, sanitize_filename, sanitize_ident};
17use crate::field_access::{FieldResolver, SwiftFirstClassMap};
18use crate::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
19use alef_core::backend::GeneratedFile;
20use alef_core::config::ResolvedCrateConfig;
21use alef_core::hash::{self, CommentStyle};
22use alef_core::template_versions::toolchain;
23use anyhow::Result;
24use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
25use std::collections::HashMap;
26use std::collections::HashSet;
27use std::fmt::Write as FmtWrite;
28use std::path::PathBuf;
29
30use super::E2eCodegen;
31use super::client;
32
33pub struct SwiftE2eCodegen;
35
36impl E2eCodegen for SwiftE2eCodegen {
37 fn generate(
38 &self,
39 groups: &[FixtureGroup],
40 e2e_config: &E2eConfig,
41 config: &ResolvedCrateConfig,
42 type_defs: &[alef_core::ir::TypeDef],
43 enums: &[alef_core::ir::EnumDef],
44 ) -> Result<Vec<GeneratedFile>> {
45 let lang = self.language_name();
46 let output_base = PathBuf::from(e2e_config.effective_output()).join("swift_e2e");
54
55 let mut files = Vec::new();
56
57 let call = &e2e_config.call;
59 let overrides = call.overrides.get(lang);
60 let function_name = overrides
61 .and_then(|o| o.function.as_ref())
62 .cloned()
63 .unwrap_or_else(|| call.function.clone());
64 let result_var = &call.result_var;
65 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
66
67 let swift_pkg = e2e_config.resolve_package("swift");
69 let pkg_name = swift_pkg
70 .as_ref()
71 .and_then(|p| p.name.as_ref())
72 .cloned()
73 .unwrap_or_else(|| config.name.to_upper_camel_case());
74 let pkg_path = swift_pkg
75 .as_ref()
76 .and_then(|p| p.path.as_ref())
77 .cloned()
78 .unwrap_or_else(|| "../../packages/swift".to_string());
79 let pkg_version = swift_pkg
80 .as_ref()
81 .and_then(|p| p.version.as_ref())
82 .cloned()
83 .or_else(|| config.resolved_version())
84 .unwrap_or_else(|| "0.1.0".to_string());
85
86 let module_name = pkg_name.as_str();
88
89 let registry_url = config
93 .try_github_repo()
94 .map(|repo| {
95 let base = repo.trim_end_matches('/').trim_end_matches(".git");
96 format!("{base}.git")
97 })
98 .unwrap_or_else(|_| format!("https://example.invalid/{module_name}.git"));
99
100 files.push(GeneratedFile {
103 path: output_base.join("Package.swift"),
104 content: render_package_swift(module_name, ®istry_url, &pkg_path, &pkg_version, e2e_config.dep_mode),
105 generated_header: false,
106 });
107
108 let tests_base = output_base.clone();
110
111 let swift_first_class_map = build_swift_first_class_map(type_defs, enums, e2e_config);
117
118 let swift_first_class_map_ref = swift_first_class_map;
119
120 let client_factory: Option<&str> = overrides.and_then(|o| o.client_factory.as_deref());
122
123 files.push(GeneratedFile {
134 path: tests_base
135 .join("Tests")
136 .join(format!("{module_name}E2ETests"))
137 .join("TestHelpers.swift"),
138 content: render_test_helpers_swift(),
139 generated_header: true,
140 });
141
142 for group in groups {
144 let active: Vec<&Fixture> = group
145 .fixtures
146 .iter()
147 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
148 .collect();
149
150 if active.is_empty() {
151 continue;
152 }
153
154 let class_name = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
155 let filename = format!("{class_name}.swift");
156 let content = render_test_file(
157 &group.category,
158 &active,
159 e2e_config,
160 module_name,
161 &class_name,
162 &function_name,
163 result_var,
164 &e2e_config.call.args,
165 result_is_simple,
166 client_factory,
167 &swift_first_class_map_ref,
168 );
169 files.push(GeneratedFile {
170 path: tests_base
171 .join("Tests")
172 .join(format!("{module_name}E2ETests"))
173 .join(filename),
174 content,
175 generated_header: true,
176 });
177 }
178
179 Ok(files)
180 }
181
182 fn language_name(&self) -> &'static str {
183 "swift"
184 }
185}
186
187const SWIFT_FORMAT_IGNORE_DIRECTIVE: &str = "// swift-format-ignore-file\n\n";
203
204fn render_test_helpers_swift() -> String {
209 let header = hash::header(CommentStyle::DoubleSlash);
210 let ignore = SWIFT_FORMAT_IGNORE_DIRECTIVE;
211 format!(
212 r#"{header}{ignore}import Foundation
213import RustBridge
214
215// Make `RustString` print its content in XCTest failure output. Without this,
216// every error thrown from the swift-bridge layer surfaces as
217// `caught error: "RustBridge.RustString"` with the actual message hidden
218// inside the opaque class instance. The `@retroactive` keyword acknowledges
219// that the conformed-to protocol (`CustomStringConvertible`) and the
220// conforming type (`RustString`) both live outside this module — required by
221// Swift 6 to silence the retroactive-conformance warning. swift-bridge does
222// not give `RustString` a `description` of its own, so there is no conflict.
223extension RustString: @retroactive CustomStringConvertible {{
224 public var description: String {{ self.toString() }}
225}}
226"#
227 )
228}
229
230fn render_package_swift(
231 module_name: &str,
232 registry_url: &str,
233 pkg_path: &str,
234 pkg_version: &str,
235 dep_mode: crate::config::DependencyMode,
236) -> String {
237 let min_macos = toolchain::SWIFT_MIN_MACOS;
238
239 let (dep_block, product_dep) = match dep_mode {
243 crate::config::DependencyMode::Registry => {
244 let dep = format!(r#" .package(url: "{registry_url}", from: "{pkg_version}")"#);
245 let pkg_id = registry_url
246 .trim_end_matches('/')
247 .trim_end_matches(".git")
248 .split('/')
249 .next_back()
250 .unwrap_or(module_name);
251 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
252 (dep, prod)
253 }
254 crate::config::DependencyMode::Local => {
255 let pkg_id = pkg_path.trim_end_matches('/').rsplit('/').next().unwrap_or(module_name);
262 let dep = format!(r#" .package(path: "{pkg_path}")"#);
263 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
264 (dep, prod)
265 }
266 };
267 let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
270 let min_ios = toolchain::SWIFT_MIN_IOS;
271 let min_ios_major = min_ios.split('.').next().unwrap_or(min_ios);
272 format!(
276 r#"// swift-tools-version: 6.0
277import PackageDescription
278
279let package = Package(
280 name: "E2eSwift",
281 platforms: [
282 .macOS(.v{min_macos_major}),
283 .iOS(.v{min_ios_major}),
284 ],
285 dependencies: [
286{dep_block},
287 ],
288 targets: [
289 .testTarget(
290 name: "{module_name}E2ETests",
291 dependencies: [{product_dep}]
292 ),
293 ]
294)
295"#
296 )
297}
298
299#[allow(clippy::too_many_arguments)]
300fn render_test_file(
301 category: &str,
302 fixtures: &[&Fixture],
303 e2e_config: &E2eConfig,
304 module_name: &str,
305 class_name: &str,
306 function_name: &str,
307 result_var: &str,
308 args: &[crate::config::ArgMapping],
309 result_is_simple: bool,
310 client_factory: Option<&str>,
311 swift_first_class_map: &SwiftFirstClassMap,
312) -> String {
313 let needs_chdir = fixtures.iter().any(|f| {
320 let call_config =
321 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
322 call_config
323 .args
324 .iter()
325 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
326 });
327
328 let mut out = String::new();
329 out.push_str(&hash::header(CommentStyle::DoubleSlash));
330 out.push_str(SWIFT_FORMAT_IGNORE_DIRECTIVE);
331 let _ = writeln!(out, "import XCTest");
332 let _ = writeln!(out, "import Foundation");
333 let _ = writeln!(out, "import {module_name}");
334 let _ = writeln!(out, "import RustBridge");
335 let _ = writeln!(out);
336 let _ = writeln!(out, "/// E2e tests for category: {category}.");
337 let _ = writeln!(out, "final class {class_name}: XCTestCase {{");
338
339 if needs_chdir {
340 let _ = writeln!(out, " override class func setUp() {{");
348 let _ = writeln!(out, " super.setUp()");
349 let _ = writeln!(out, " let _testDocs = URL(fileURLWithPath: #filePath)");
350 let _ = writeln!(out, " .deletingLastPathComponent() // <Module>Tests/");
351 let _ = writeln!(out, " .deletingLastPathComponent() // Tests/");
352 let _ = writeln!(out, " .deletingLastPathComponent() // swift/");
353 let _ = writeln!(out, " .deletingLastPathComponent() // packages/");
354 let _ = writeln!(out, " .deletingLastPathComponent() // <repo root>");
355 let _ = writeln!(
356 out,
357 " .appendingPathComponent(\"{}\")",
358 e2e_config.test_documents_dir
359 );
360 let _ = writeln!(
361 out,
362 " if FileManager.default.fileExists(atPath: _testDocs.path) {{"
363 );
364 let _ = writeln!(
365 out,
366 " FileManager.default.changeCurrentDirectoryPath(_testDocs.path)"
367 );
368 let _ = writeln!(out, " }}");
369 let _ = writeln!(out, " }}");
370 let _ = writeln!(out);
371 }
372
373 for fixture in fixtures {
374 if fixture.is_http_test() {
375 render_http_test_method(&mut out, fixture);
376 } else {
377 render_test_method(
378 &mut out,
379 fixture,
380 e2e_config,
381 function_name,
382 result_var,
383 args,
384 result_is_simple,
385 client_factory,
386 swift_first_class_map,
387 module_name,
388 );
389 }
390 let _ = writeln!(out);
391 }
392
393 let _ = writeln!(out, "}}");
394 out
395}
396
397struct SwiftTestClientRenderer;
404
405impl client::TestClientRenderer for SwiftTestClientRenderer {
406 fn language_name(&self) -> &'static str {
407 "swift"
408 }
409
410 fn sanitize_test_name(&self, id: &str) -> String {
411 sanitize_ident(id).to_upper_camel_case()
413 }
414
415 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
421 let _ = writeln!(out, " /// {description}");
422 let _ = writeln!(out, " func test{fn_name}() throws {{");
423 if let Some(reason) = skip_reason {
424 let escaped = escape_swift(reason);
425 let _ = writeln!(out, " try XCTSkipIf(true, \"{escaped}\")");
426 }
427 }
428
429 fn render_test_close(&self, out: &mut String) {
430 let _ = writeln!(out, " }}");
431 }
432
433 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
440 let method = ctx.method.to_uppercase();
441 let fixture_path = escape_swift(ctx.path);
442
443 let _ = writeln!(
444 out,
445 " let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
446 );
447 let _ = writeln!(
448 out,
449 " var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
450 );
451 let _ = writeln!(out, " _req.httpMethod = \"{method}\"");
452
453 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
455 header_pairs.sort_by_key(|(k, _)| k.as_str());
456 for (k, v) in &header_pairs {
457 let expanded_v = expand_fixture_templates(v);
458 let ek = escape_swift(k);
459 let ev = escape_swift(&expanded_v);
460 let _ = writeln!(out, " _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
461 }
462
463 if let Some(body) = ctx.body {
465 let json_str = serde_json::to_string(body).unwrap_or_default();
466 let escaped_body = escape_swift(&json_str);
467 let _ = writeln!(out, " _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
468 let _ = writeln!(
469 out,
470 " _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
471 );
472 }
473
474 let _ = writeln!(out, " var {}: HTTPURLResponse?", ctx.response_var);
475 let _ = writeln!(out, " var _responseData: Data?");
476 let _ = writeln!(out, " let _sema = DispatchSemaphore(value: 0)");
477 let _ = writeln!(
478 out,
479 " URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
480 );
481 let _ = writeln!(out, " {} = resp as? HTTPURLResponse", ctx.response_var);
482 let _ = writeln!(out, " _responseData = data");
483 let _ = writeln!(out, " _sema.signal()");
484 let _ = writeln!(out, " }}.resume()");
485 let _ = writeln!(out, " _sema.wait()");
486 let _ = writeln!(out, " let _resp = try XCTUnwrap({})", ctx.response_var);
487 }
488
489 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
490 let _ = writeln!(out, " XCTAssertEqual(_resp.statusCode, {status})");
491 }
492
493 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
494 let lower_name = name.to_lowercase();
495 let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
496 match expected {
497 "<<present>>" => {
498 let _ = writeln!(out, " XCTAssertNotNil({header_expr})");
499 }
500 "<<absent>>" => {
501 let _ = writeln!(out, " XCTAssertNil({header_expr})");
502 }
503 "<<uuid>>" => {
504 let _ = writeln!(out, " let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
505 let _ = writeln!(
506 out,
507 " XCTAssertNotNil(_hdrVal_{lower_name}.range(of: #\"^[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}$\"#, options: .regularExpression))"
508 );
509 }
510 exact => {
511 let escaped = escape_swift(exact);
512 let _ = writeln!(out, " XCTAssertEqual({header_expr}, \"{escaped}\")");
513 }
514 }
515 }
516
517 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
518 if let serde_json::Value::String(s) = expected {
519 let escaped = escape_swift(s);
520 let _ = writeln!(
521 out,
522 " let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
523 );
524 let _ = writeln!(
525 out,
526 " XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
527 );
528 } else {
529 let json_str = serde_json::to_string(expected).unwrap_or_default();
530 let escaped = escape_swift(&json_str);
531 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
532 let _ = writeln!(
533 out,
534 " let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
535 );
536 let _ = writeln!(
537 out,
538 " let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
539 );
540 let _ = writeln!(
541 out,
542 " XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
543 );
544 }
545 }
546
547 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
548 if let Some(obj) = expected.as_object() {
549 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
550 let _ = writeln!(
551 out,
552 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
553 );
554 for (key, val) in obj {
555 let escaped_key = escape_swift(key);
556 let swift_val = json_to_swift(val);
557 let _ = writeln!(
558 out,
559 " XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
560 );
561 }
562 }
563 }
564
565 fn render_assert_validation_errors(
566 &self,
567 out: &mut String,
568 _response_var: &str,
569 errors: &[ValidationErrorExpectation],
570 ) {
571 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
572 let _ = writeln!(
573 out,
574 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
575 );
576 let _ = writeln!(
577 out,
578 " let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
579 );
580 for ve in errors {
581 let escaped_msg = escape_swift(&ve.msg);
582 let _ = writeln!(
583 out,
584 " XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
585 );
586 }
587 }
588}
589
590fn render_http_test_method(out: &mut String, fixture: &Fixture) {
595 let Some(http) = &fixture.http else {
596 return;
597 };
598
599 if http.expected_response.status_code == 101 {
601 let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
602 let description = fixture.description.replace('"', "\\\"");
603 let _ = writeln!(out, " /// {description}");
604 let _ = writeln!(out, " func test{method_name}() throws {{");
605 let _ = writeln!(
606 out,
607 " try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
608 );
609 let _ = writeln!(out, " }}");
610 return;
611 }
612
613 client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
614}
615
616#[allow(clippy::too_many_arguments)]
621fn render_test_method(
622 out: &mut String,
623 fixture: &Fixture,
624 e2e_config: &E2eConfig,
625 _function_name: &str,
626 _result_var: &str,
627 _args: &[crate::config::ArgMapping],
628 result_is_simple: bool,
629 global_client_factory: Option<&str>,
630 swift_first_class_map: &SwiftFirstClassMap,
631 module_name: &str,
632) {
633 let call_config = e2e_config.resolve_call_for_fixture(
635 fixture.call.as_deref(),
636 &fixture.id,
637 &fixture.resolved_category(),
638 &fixture.tags,
639 &fixture.input,
640 );
641 let call_field_resolver = FieldResolver::new_with_swift_first_class(
643 e2e_config.effective_fields(call_config),
644 e2e_config.effective_fields_optional(call_config),
645 e2e_config.effective_result_fields(call_config),
646 e2e_config.effective_fields_array(call_config),
647 e2e_config.effective_fields_method_calls(call_config),
648 &HashMap::new(),
649 swift_first_class_map.clone(),
650 );
651 let field_resolver = &call_field_resolver;
652 let enum_fields = e2e_config.effective_fields_enum(call_config);
653 let lang = "swift";
654 let call_overrides = call_config.overrides.get(lang);
655 let function_name = call_overrides
656 .and_then(|o| o.function.as_ref())
657 .cloned()
658 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
659 let client_factory: Option<&str> = call_overrides
661 .and_then(|o| o.client_factory.as_deref())
662 .or(global_client_factory);
663 let result_var = &call_config.result_var;
664 let args = &call_config.args;
665 let result_is_bytes_any_lang =
672 call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
673 let result_is_simple = call_config.result_is_simple
674 || call_overrides.is_some_and(|o| o.result_is_simple)
675 || result_is_simple
676 || result_is_bytes_any_lang;
677 let result_is_array = call_config.result_is_array;
678 let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);
683
684 let method_name = fixture.id.to_upper_camel_case();
685 let description = &fixture.description;
686 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
687 let is_async = call_config.r#async;
688
689 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
691 let collect_snippet_opt = if is_streaming && !expects_error {
692 crate::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(lang, result_var, "chunks")
693 } else {
694 None
695 };
696 if is_streaming && !expects_error && collect_snippet_opt.is_none() {
703 if is_async {
704 let _ = writeln!(out, " func test{method_name}() async throws {{");
705 } else {
706 let _ = writeln!(out, " func test{method_name}() throws {{");
707 }
708 let _ = writeln!(out, " // {description}");
709 let _ = writeln!(
710 out,
711 " try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
712 fixture.id
713 );
714 let _ = writeln!(out, " }}");
715 return;
716 }
717 let collect_snippet = collect_snippet_opt.unwrap_or_default();
718 let collect_snippet = if collect_snippet.is_empty() {
725 collect_snippet
726 } else {
727 collect_snippet.replace("[ChatCompletionChunk]", &format!("[{module_name}.ChatCompletionChunk]"))
728 };
729
730 let has_unresolvable_json_object_arg = {
737 let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
738 options_via.is_none() && args.iter().any(|a| a.arg_type == "json_object" && a.name != "config")
739 };
740
741 if has_unresolvable_json_object_arg {
742 if is_async {
743 let _ = writeln!(out, " func test{method_name}() async throws {{");
744 } else {
745 let _ = writeln!(out, " func test{method_name}() throws {{");
746 }
747 let _ = writeln!(out, " // {description}");
748 let _ = writeln!(
749 out,
750 " try XCTSkipIf(true, \"swift: json_object request construction requires options_via configuration (fixture: {})\");",
751 fixture.id
752 );
753 let _ = writeln!(out, " }}");
754 return;
755 }
756
757 let mut visitor_setup_lines: Vec<String> = Vec::new();
761 let visitor_handle_expr: Option<String> = fixture
762 .visitor
763 .as_ref()
764 .map(|spec| super::swift_visitors::build_swift_visitor(&mut visitor_setup_lines, spec, &fixture.id));
765
766 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
770
771 let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
776 let per_call = call_overrides.map(|o| &o.enum_fields);
777 if let Some(pc) = per_call {
778 if !pc.is_empty() {
779 let mut merged = enum_fields.clone();
780 merged.extend(pc.keys().cloned());
781 std::borrow::Cow::Owned(merged)
782 } else {
783 std::borrow::Cow::Borrowed(enum_fields)
784 }
785 } else {
786 std::borrow::Cow::Borrowed(enum_fields)
787 }
788 };
789
790 let options_via_str: Option<&str> = call_overrides.and_then(|o| o.options_via.as_deref());
791 let options_type_str: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
792 let handle_config_fn_owned: Option<String> = call_config
796 .overrides
797 .get("c")
798 .and_then(|c| c.c_engine_factory.as_deref())
799 .map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
800 let (mut setup_lines, args_str) = build_args_and_setup(
801 &fixture.input,
802 args,
803 &fixture.id,
804 fixture.has_host_root_route(),
805 &function_name,
806 options_via_str,
807 options_type_str,
808 handle_config_fn_owned.as_deref(),
809 visitor_handle_expr.as_deref(),
810 client_factory.is_some(),
811 module_name,
812 );
813 if !visitor_setup_lines.is_empty() {
815 visitor_setup_lines.extend(setup_lines);
816 setup_lines = visitor_setup_lines;
817 }
818
819 let args_str = if extra_args.is_empty() {
821 args_str
822 } else if args_str.is_empty() {
823 extra_args.join(", ")
824 } else {
825 format!("{args_str}, {}", extra_args.join(", "))
826 };
827
828 let has_mock = fixture.mock_response.is_some();
833 let (call_setup, call_expr) = if let Some(_factory) = client_factory {
834 let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
835 let mock_url = if fixture.has_host_root_route() {
836 format!(
837 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\")",
838 fixture.id
839 )
840 } else {
841 format!(
842 "ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\"",
843 fixture.id
844 )
845 };
846 let client_constructor = if has_mock {
847 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
848 } else {
849 if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
851 format!(
852 "let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n \
853 let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n \
854 let _client = try DefaultClient(apiKey: _apiKey ?? \"test-key\", baseUrl: _baseUrl)"
855 )
856 } else {
857 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
858 }
859 };
860 let expr = if is_async {
861 format!("try await _client.{function_name}({args_str})")
862 } else {
863 format!("try _client.{function_name}({args_str})")
864 };
865 (Some(client_constructor), expr)
866 } else {
867 let expr = if is_async {
870 format!("try await {module_name}.{function_name}({args_str})")
871 } else {
872 format!("try {module_name}.{function_name}({args_str})")
873 };
874 (None, expr)
875 };
876 let _ = function_name;
878
879 if is_async {
880 let _ = writeln!(out, " func test{method_name}() async throws {{");
881 } else {
882 let _ = writeln!(out, " func test{method_name}() throws {{");
883 }
884 let _ = writeln!(out, " // {description}");
885
886 if expects_error {
887 if is_async {
891 let _ = writeln!(out, " do {{");
896 for line in &setup_lines {
897 let _ = writeln!(out, " {line}");
898 }
899 if let Some(setup) = &call_setup {
900 let _ = writeln!(out, " {setup}");
901 }
902 let _ = writeln!(out, " _ = {call_expr}");
903 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
904 let _ = writeln!(out, " }} catch {{");
905 let _ = writeln!(out, " // success");
906 let _ = writeln!(out, " }}");
907 } else {
908 let _ = writeln!(out, " do {{");
915 for line in &setup_lines {
916 let _ = writeln!(out, " {line}");
917 }
918 if let Some(setup) = &call_setup {
919 let _ = writeln!(out, " {setup}");
920 }
921 let _ = writeln!(out, " _ = {call_expr}");
922 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
923 let _ = writeln!(out, " }} catch {{");
924 let _ = writeln!(out, " // success");
925 let _ = writeln!(out, " }}");
926 }
927 let _ = writeln!(out, " }}");
928 return;
929 }
930
931 for line in &setup_lines {
932 let _ = writeln!(out, " {line}");
933 }
934
935 if let Some(setup) = &call_setup {
937 let _ = writeln!(out, " {setup}");
938 }
939
940 let _ = writeln!(out, " let {result_var} = {call_expr}");
941
942 if !collect_snippet.is_empty() {
945 for line in collect_snippet.lines() {
946 let _ = writeln!(out, " {line}");
947 }
948 }
949
950 let fixture_root_type: Option<String> = swift_call_result_type(call_config);
955 let fixture_resolver = field_resolver.with_swift_root_type(fixture_root_type);
956
957 for assertion in &fixture.assertions {
958 let mut assertion_out = String::new();
959 render_assertion(
960 &mut assertion_out,
961 assertion,
962 result_var,
963 &fixture_resolver,
964 result_is_simple,
965 result_is_array,
966 result_is_option,
967 &effective_enum_fields,
968 is_streaming,
969 );
970 for unqualified in ["StreamToolCall", "ToolCall"] {
978 assertion_out =
979 assertion_out.replace(&format!("[{unqualified}]"), &format!("[{module_name}.{unqualified}]"));
980 }
981 out.push_str(&assertion_out);
982 }
983
984 let _ = writeln!(out, " }}");
985}
986
987#[allow(clippy::too_many_arguments)]
988fn build_args_and_setup(
1002 input: &serde_json::Value,
1003 args: &[crate::config::ArgMapping],
1004 fixture_id: &str,
1005 has_host_root_route: bool,
1006 function_name: &str,
1007 options_via: Option<&str>,
1008 options_type: Option<&str>,
1009 handle_config_fn: Option<&str>,
1010 visitor_handle_expr: Option<&str>,
1011 is_method_call: bool,
1012 module_name: &str,
1013) -> (Vec<String>, String) {
1014 if args.is_empty() {
1015 return (Vec::new(), String::new());
1016 }
1017
1018 let mut setup_lines: Vec<String> = Vec::new();
1019 let mut parts: Vec<(usize, String)> = Vec::new();
1020
1021 let later_emits: Vec<bool> = (0..args.len())
1026 .map(|i| {
1027 args.iter().skip(i + 1).any(|a| {
1028 let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
1029 let v = input.get(f);
1030 let has_value = matches!(v, Some(x) if !x.is_null());
1031 has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
1032 })
1033 })
1034 .collect();
1035
1036 for (idx, arg) in args.iter().enumerate() {
1037 if arg.arg_type == "mock_url" {
1038 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
1039 let url_expr = if has_host_root_route {
1040 format!(
1041 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\")"
1042 )
1043 } else {
1044 format!("ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"")
1045 };
1046 setup_lines.push(format!("let {} = {url_expr}", arg.name));
1047 parts.push((idx, arg.name.clone()));
1048 continue;
1049 }
1050
1051 if arg.arg_type == "handle" {
1052 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1053 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1054 let config_val = input.get(field);
1055 let has_config = config_val
1056 .is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
1057 if has_config {
1058 if let Some(from_json_fn) = handle_config_fn {
1059 let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
1060 let escaped = escape_swift_str(&json_str);
1061 let config_var = format!("{}Config", arg.name.to_lower_camel_case());
1062 setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
1063 setup_lines.push(format!("let {var_name} = try createEngine({config_var})"));
1064 } else {
1065 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
1066 }
1067 } else {
1068 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
1069 }
1070 parts.push((idx, var_name));
1071 continue;
1072 }
1073
1074 if arg.arg_type == "bytes" {
1079 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1080 let val = input.get(field);
1081 match val {
1082 None | Some(serde_json::Value::Null) if arg.optional => {
1083 if later_emits[idx] {
1084 parts.push((idx, "nil".to_string()));
1085 }
1086 }
1087 None | Some(serde_json::Value::Null) => {
1088 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1089 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1090 parts.push((idx, var_name));
1091 }
1092 Some(serde_json::Value::String(s)) => {
1093 let escaped = escape_swift(s);
1094 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1095 let data_var = format!("{}Data", arg.name.to_lower_camel_case());
1096 setup_lines.push(format!(
1097 "let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
1098 ));
1099 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1100 setup_lines.push(format!("for _byte in {data_var} {{ {var_name}.push(value: _byte) }}"));
1101 parts.push((idx, var_name));
1102 }
1103 Some(serde_json::Value::Array(arr)) => {
1104 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1105 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1106 for v in arr {
1107 if let Some(n) = v.as_u64() {
1108 setup_lines.push(format!("{var_name}.push(value: UInt8({n}))"));
1109 }
1110 }
1111 parts.push((idx, var_name));
1112 }
1113 Some(other) => {
1114 let json_str = serde_json::to_string(other).unwrap_or_default();
1116 let escaped = escape_swift(&json_str);
1117 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1118 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1119 setup_lines.push(format!(
1120 "for _byte in Array(\"{escaped}\".utf8) {{ {var_name}.push(value: _byte) }}"
1121 ));
1122 parts.push((idx, var_name));
1123 }
1124 }
1125 continue;
1126 }
1127
1128 let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
1134 let is_batch_fn = function_name.starts_with("batch") || function_name.starts_with("Batch");
1135 if is_config_arg && !is_batch_fn {
1136 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1137 let val = input.get(field);
1138 let json_str = match val {
1139 None | Some(serde_json::Value::Null) => "{}".to_string(),
1140 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1141 };
1142 let escaped = escape_swift(&json_str);
1143 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1144 let from_json_fn = if let Some(type_name) = options_type {
1146 format!("{}FromJson", type_name.to_lower_camel_case())
1147 } else {
1148 "extractionConfigFromJson".to_string()
1149 };
1150 setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1151 parts.push((idx, var_name));
1152 continue;
1153 }
1154
1155 if arg.arg_type == "json_object" && options_via == Some("from_json") {
1162 if let Some(type_name) = options_type {
1163 let resolved_val = super::resolve_field(input, &arg.field);
1164 let json_str = match resolved_val {
1165 serde_json::Value::Null => "{}".to_string(),
1166 v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1167 };
1168 let escaped = escape_swift(&json_str);
1169 let var_name = format!("_{}", arg.name.to_lower_camel_case());
1170 if let Some(handle_expr) = visitor_handle_expr {
1171 let with_visitor_fn = format!("{}FromJsonWithVisitor", type_name.to_lower_camel_case());
1176 let handle_var = format!("_visitorHandle_{}", var_name.trim_start_matches('_'));
1177 setup_lines.push(format!("let {handle_var} = {handle_expr}"));
1178 setup_lines.push(format!(
1179 "let {var_name} = try {module_name}.{with_visitor_fn}(\"{escaped}\", {handle_var})"
1180 ));
1181 } else {
1182 let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
1183 setup_lines.push(format!("let {var_name} = try {module_name}.{from_json_fn}(\"{escaped}\")"));
1184 }
1185 parts.push((idx, var_name));
1186 continue;
1187 }
1188 }
1189
1190 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1191 let val = input.get(field);
1192 match val {
1193 None | Some(serde_json::Value::Null) if arg.optional => {
1194 if later_emits[idx] {
1198 parts.push((idx, "nil".to_string()));
1199 }
1200 }
1201 None | Some(serde_json::Value::Null) => {
1202 let default_val = match arg.arg_type.as_str() {
1203 "string" => "\"\"".to_string(),
1204 "int" | "integer" => "0".to_string(),
1205 "float" | "number" => "0.0".to_string(),
1206 "bool" | "boolean" => "false".to_string(),
1207 _ => "nil".to_string(),
1208 };
1209 parts.push((idx, default_val));
1210 }
1211 Some(v) => {
1212 parts.push((idx, json_to_swift(v)));
1213 }
1214 }
1215 }
1216
1217 let args_str = parts
1221 .into_iter()
1222 .map(|(idx, val)| {
1223 if is_method_call {
1224 val
1225 } else {
1226 format!("{}: {}", args[idx].name, val)
1227 }
1228 })
1229 .collect::<Vec<_>>()
1230 .join(", ");
1231 (setup_lines, args_str)
1232}
1233
1234#[allow(clippy::too_many_arguments)]
1235fn render_assertion(
1236 out: &mut String,
1237 assertion: &Assertion,
1238 result_var: &str,
1239 field_resolver: &FieldResolver,
1240 result_is_simple: bool,
1241 result_is_array: bool,
1242 result_is_option: bool,
1243 enum_fields: &HashSet<String>,
1244 is_streaming: bool,
1245) {
1246 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1251 if let Some(f) = &assertion.field {
1256 let is_streaming_usage_path =
1257 is_streaming && (f == "usage" || (f.starts_with("usage.") || f.starts_with("usage[")));
1258 if !f.is_empty()
1259 && (crate::codegen::streaming_assertions::is_streaming_virtual_field(f) || is_streaming_usage_path)
1260 {
1261 if let Some(expr) =
1262 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "swift", "chunks")
1263 {
1264 let line = match assertion.assertion_type.as_str() {
1265 "count_min" => {
1266 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1267 format!(" XCTAssertGreaterThanOrEqual(chunks.count, {n})\n")
1268 } else {
1269 String::new()
1270 }
1271 }
1272 "count_equals" => {
1273 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1274 format!(" XCTAssertEqual(chunks.count, {n})\n")
1275 } else {
1276 String::new()
1277 }
1278 }
1279 "equals" => {
1280 if let Some(serde_json::Value::String(s)) = &assertion.value {
1281 let escaped = escape_swift(s);
1282 format!(" XCTAssertEqual({expr}, \"{escaped}\")\n")
1283 } else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
1284 format!(" XCTAssertEqual({expr}, {b})\n")
1285 } else {
1286 String::new()
1287 }
1288 }
1289 "not_empty" => {
1290 format!(" XCTAssertFalse({expr}.isEmpty, \"expected non-empty\")\n")
1291 }
1292 "is_empty" => {
1293 format!(" XCTAssertTrue({expr}.isEmpty, \"expected empty\")\n")
1294 }
1295 "is_true" => {
1296 format!(" XCTAssertTrue({expr})\n")
1297 }
1298 "is_false" => {
1299 format!(" XCTAssertFalse({expr})\n")
1300 }
1301 "greater_than" => {
1302 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1303 format!(" XCTAssertGreaterThan(chunks.count, {n})\n")
1304 } else {
1305 String::new()
1306 }
1307 }
1308 "contains" => {
1309 if let Some(serde_json::Value::String(s)) = &assertion.value {
1310 let escaped = escape_swift(s);
1311 format!(
1312 " XCTAssertTrue({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\")\n"
1313 )
1314 } else {
1315 String::new()
1316 }
1317 }
1318 _ => format!(
1319 " // streaming field '{f}': assertion type '{}' not rendered\n",
1320 assertion.assertion_type
1321 ),
1322 };
1323 if !line.is_empty() {
1324 out.push_str(&line);
1325 }
1326 }
1327 return;
1328 }
1329 }
1330
1331 if let Some(f) = &assertion.field {
1333 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1334 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1335 return;
1336 }
1337 }
1338
1339 if let Some(f) = &assertion.field {
1344 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1345 let _ = writeln!(
1346 out,
1347 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Swift)"
1348 );
1349 return;
1350 }
1351 }
1352
1353 let field_is_enum = assertion
1355 .field
1356 .as_deref()
1357 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1358
1359 let field_is_optional = assertion.field.as_deref().is_some_and(|f| {
1360 !f.is_empty() && (field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f)))
1361 });
1362 let field_is_array = assertion.field.as_deref().is_some_and(|f| {
1363 !f.is_empty()
1364 && (field_resolver.is_array(f)
1365 || field_resolver.is_array(field_resolver.resolve(f))
1366 || field_resolver.is_collection_root(f)
1367 || field_resolver.is_collection_root(field_resolver.resolve(f)))
1368 });
1369
1370 let field_expr_raw = if result_is_simple {
1371 result_var.to_string()
1372 } else {
1373 match &assertion.field {
1374 Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
1375 _ => result_var.to_string(),
1376 }
1377 };
1378
1379 let local_suffix = {
1389 use std::hash::{Hash, Hasher};
1390 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1391 assertion.field.hash(&mut hasher);
1392 assertion
1393 .value
1394 .as_ref()
1395 .map(|v| v.to_string())
1396 .unwrap_or_default()
1397 .hash(&mut hasher);
1398 format!(
1399 "{}_{:x}",
1400 assertion.assertion_type.replace(['-', '.'], "_"),
1401 hasher.finish() & 0xffff_ffff,
1402 )
1403 };
1404 let (vec_setup, field_expr, is_map_subscript) = materialise_vec_temporaries(&field_expr_raw, &local_suffix);
1405 let field_uses_traversal = assertion.field.as_deref().is_some_and(|f| f.contains("[]."));
1410 let traversal_skips_field_expr = field_uses_traversal
1411 && matches!(
1412 assertion.assertion_type.as_str(),
1413 "contains" | "not_contains" | "not_empty" | "is_empty"
1414 );
1415 if !traversal_skips_field_expr {
1416 for line in &vec_setup {
1417 let _ = writeln!(out, " {line}");
1418 }
1419 }
1420
1421 let accessor_is_optional = field_expr.contains("?.");
1427 let leaf_is_property_access = {
1435 let trimmed = field_expr.trim_end_matches('?');
1436 let last_segment = trimmed.rsplit_once('.').map(|(_, s)| s).unwrap_or(trimmed);
1438 let last_segment = last_segment.split('[').next().unwrap_or(last_segment);
1439 !last_segment.ends_with(')') && !last_segment.is_empty()
1440 };
1441
1442 let string_expr = if is_map_subscript {
1451 format!("({field_expr} ?? \"\")")
1455 } else if leaf_is_property_access {
1456 if field_is_enum && (field_is_optional || accessor_is_optional) {
1461 format!("(({field_expr})?.rawValue ?? \"\")")
1465 } else if field_is_enum {
1466 format!("{field_expr}.rawValue")
1467 } else if field_is_optional || accessor_is_optional {
1468 format!("({field_expr} ?? \"\")")
1469 } else {
1470 field_expr.to_string()
1471 }
1472 } else if field_is_enum && (field_is_optional || accessor_is_optional) {
1473 format!("({field_expr}?.toString() ?? \"\")")
1476 } else if field_is_enum {
1477 format!("{field_expr}.toString()")
1482 } else if field_is_optional {
1483 format!("({field_expr}?.toString() ?? \"\")")
1485 } else if accessor_is_optional {
1486 format!("({field_expr}.toString() ?? \"\")")
1489 } else {
1490 format!("{field_expr}.toString()")
1491 };
1492
1493 match assertion.assertion_type.as_str() {
1494 "equals" => {
1495 if let Some(expected) = &assertion.value {
1496 let swift_val = json_to_swift(expected);
1497 if expected.is_string() {
1498 if field_is_enum {
1499 let trim_expr =
1503 format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1504 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1505 } else {
1506 let trim_expr =
1511 format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1512 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1513 }
1514 } else {
1515 let cast_swift_val = swift_numeric_literal_cast(&field_expr, &swift_val);
1517 let _ = writeln!(out, " XCTAssertEqual({field_expr}, {cast_swift_val})");
1518 }
1519 }
1520 }
1521 "contains" => {
1522 if let Some(expected) = &assertion.value {
1523 let swift_val = json_to_swift(expected);
1524 let no_field = assertion.field.as_deref().is_none_or(|f| f.is_empty());
1527 if result_is_simple && result_is_array && no_field {
1528 let _ = writeln!(
1531 out,
1532 " XCTAssertTrue({result_var}.map {{ $0.as_str().toString() }}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1533 );
1534 } else {
1535 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1537 if let Some(dot) = f.find("[].") {
1538 let array_part = &f[..dot];
1539 let elem_part = &f[dot + 3..];
1540 let line = swift_traversal_contains_assert(
1541 array_part,
1542 elem_part,
1543 f,
1544 &swift_val,
1545 result_var,
1546 false,
1547 &format!("expected to contain: \\({swift_val})"),
1548 enum_fields,
1549 field_resolver,
1550 );
1551 let _ = writeln!(out, "{line}");
1552 true
1553 } else {
1554 false
1555 }
1556 } else {
1557 false
1558 };
1559 if !traversal_handled {
1560 let field_is_array = assertion
1562 .field
1563 .as_deref()
1564 .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
1565 if field_is_array {
1566 let contains_expr =
1567 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1568 let _ = writeln!(
1569 out,
1570 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1571 );
1572 } else if field_is_enum {
1573 let _ = writeln!(
1578 out,
1579 " XCTAssertTrue({swift_val}.isEmpty || {string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1580 );
1581 } else {
1582 let _ = writeln!(
1585 out,
1586 " XCTAssertTrue({swift_val}.isEmpty || {string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1587 );
1588 }
1589 }
1590 }
1591 }
1592 }
1593 "contains_all" => {
1594 if let Some(values) = &assertion.values {
1595 if let Some(f) = assertion.field.as_deref() {
1597 if let Some(dot) = f.find("[].") {
1598 let array_part = &f[..dot];
1599 let elem_part = &f[dot + 3..];
1600 for val in values {
1601 let swift_val = json_to_swift(val);
1602 let line = swift_traversal_contains_assert(
1603 array_part,
1604 elem_part,
1605 f,
1606 &swift_val,
1607 result_var,
1608 false,
1609 &format!("expected to contain: \\({swift_val})"),
1610 enum_fields,
1611 field_resolver,
1612 );
1613 let _ = writeln!(out, "{line}");
1614 }
1615 } else {
1617 let field_is_array = field_resolver.is_array(field_resolver.resolve(f));
1619 if field_is_array {
1620 let contains_expr =
1621 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1622 for val in values {
1623 let swift_val = json_to_swift(val);
1624 let _ = writeln!(
1625 out,
1626 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1627 );
1628 }
1629 } else if field_is_enum {
1630 for val in values {
1633 let swift_val = json_to_swift(val);
1634 let _ = writeln!(
1635 out,
1636 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1637 );
1638 }
1639 } else {
1640 for val in values {
1641 let swift_val = json_to_swift(val);
1642 let _ = writeln!(
1643 out,
1644 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1645 );
1646 }
1647 }
1648 }
1649 } else {
1650 for val in values {
1652 let swift_val = json_to_swift(val);
1653 let _ = writeln!(
1654 out,
1655 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1656 );
1657 }
1658 }
1659 }
1660 }
1661 "not_contains" => {
1662 if let Some(expected) = &assertion.value {
1663 let swift_val = json_to_swift(expected);
1664 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1666 if let Some(dot) = f.find("[].") {
1667 let array_part = &f[..dot];
1668 let elem_part = &f[dot + 3..];
1669 let line = swift_traversal_contains_assert(
1670 array_part,
1671 elem_part,
1672 f,
1673 &swift_val,
1674 result_var,
1675 true,
1676 &format!("expected NOT to contain: \\({swift_val})"),
1677 enum_fields,
1678 field_resolver,
1679 );
1680 let _ = writeln!(out, "{line}");
1681 true
1682 } else {
1683 false
1684 }
1685 } else {
1686 false
1687 };
1688 if !traversal_handled {
1689 let _ = writeln!(
1690 out,
1691 " XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
1692 );
1693 }
1694 }
1695 }
1696 "not_empty" => {
1697 let traversal_not_empty_handled = if let Some(f) = assertion.field.as_deref() {
1704 if let Some(dot) = f.find("[].") {
1705 let array_part = &f[..dot];
1706 let elem_part = &f[dot + 3..];
1707 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1708 let resolved_full = field_resolver.resolve(f);
1709 let resolved_elem_part = resolved_full
1710 .find("[].")
1711 .map(|d| &resolved_full[d + 3..])
1712 .unwrap_or(elem_part);
1713 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1714 let elem_is_enum = enum_fields.contains(f) || enum_fields.contains(resolved_full);
1715 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1716 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1717 let elem_str = if elem_is_enum {
1718 format!("{elem_accessor}.to_string().toString()")
1719 } else if elem_is_optional {
1720 format!("({elem_accessor}?.toString() ?? \"\")")
1721 } else {
1722 format!("{elem_accessor}.toString()")
1723 };
1724 let _ = writeln!(
1725 out,
1726 " XCTAssertTrue({array_accessor}.contains(where: {{ !{elem_str}.isEmpty }}), \"expected non-empty value\")"
1727 );
1728 true
1729 } else {
1730 false
1731 }
1732 } else {
1733 false
1734 };
1735 if !traversal_not_empty_handled {
1736 if bare_result_is_option {
1737 let _ = writeln!(out, " XCTAssertNotNil({result_var}, \"expected non-nil value\")");
1738 } else if field_is_optional {
1739 let _ = writeln!(out, " XCTAssertNotNil({field_expr}, \"expected non-nil value\")");
1740 } else if field_is_array {
1741 let _ = writeln!(
1742 out,
1743 " XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
1744 );
1745 } else if result_is_simple {
1746 let _ = writeln!(
1748 out,
1749 " XCTAssertFalse({result_var}.isEmpty, \"expected non-empty value\")"
1750 );
1751 } else {
1752 let count_target = swift_count_target(&field_expr, field_resolver, assertion.field.as_deref());
1767 let len_expr = if accessor_is_optional {
1768 format!("({count_target}.count ?? 0)")
1769 } else {
1770 format!("{count_target}.count")
1771 };
1772 let _ = writeln!(
1773 out,
1774 " XCTAssertGreaterThan({len_expr}, 0, \"expected non-empty value\")"
1775 );
1776 }
1777 }
1778 }
1779 "is_empty" => {
1780 if bare_result_is_option {
1781 let _ = writeln!(out, " XCTAssertNil({result_var}, \"expected nil value\")");
1782 } else if field_is_optional {
1783 let _ = writeln!(out, " XCTAssertNil({field_expr}, \"expected nil value\")");
1784 } else if field_is_array {
1785 let _ = writeln!(
1786 out,
1787 " XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
1788 );
1789 } else {
1790 let count_target = swift_count_target(&field_expr, field_resolver, assertion.field.as_deref());
1794 let len_expr = if accessor_is_optional {
1795 format!("({count_target}.count ?? 0)")
1796 } else {
1797 format!("{count_target}.count")
1798 };
1799 let _ = writeln!(out, " XCTAssertEqual({len_expr}, 0, \"expected empty value\")");
1800 }
1801 }
1802 "contains_any" => {
1803 if let Some(values) = &assertion.values {
1804 let checks: Vec<String> = values
1805 .iter()
1806 .map(|v| {
1807 let swift_val = json_to_swift(v);
1808 format!("{string_expr}.contains({swift_val})")
1809 })
1810 .collect();
1811 let joined = checks.join(" || ");
1812 let _ = writeln!(
1813 out,
1814 " XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
1815 );
1816 }
1817 }
1818 "greater_than" => {
1819 if let Some(val) = &assertion.value {
1820 let swift_val = json_to_swift(val);
1821 let field_is_optional = accessor_is_optional
1824 || assertion.field.as_deref().is_some_and(|f| {
1825 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1826 });
1827 let compare_expr = if field_is_optional {
1828 let cast_val = swift_numeric_literal_cast(&field_expr, "0");
1829 format!("({field_expr} ?? {cast_val})")
1830 } else {
1831 field_expr.clone()
1832 };
1833 let cast_swift_val = swift_numeric_literal_cast(&field_expr, &swift_val);
1834 let _ = writeln!(out, " XCTAssertGreaterThan({compare_expr}, {cast_swift_val})");
1835 }
1836 }
1837 "less_than" => {
1838 if let Some(val) = &assertion.value {
1839 let swift_val = json_to_swift(val);
1840 let field_is_optional = accessor_is_optional
1841 || assertion.field.as_deref().is_some_and(|f| {
1842 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1843 });
1844 let compare_expr = if field_is_optional {
1845 let cast_val = swift_numeric_literal_cast(&field_expr, "0");
1846 format!("({field_expr} ?? {cast_val})")
1847 } else {
1848 field_expr.clone()
1849 };
1850 let cast_swift_val = swift_numeric_literal_cast(&field_expr, &swift_val);
1851 let _ = writeln!(out, " XCTAssertLessThan({compare_expr}, {cast_swift_val})");
1852 }
1853 }
1854 "greater_than_or_equal" => {
1855 if let Some(val) = &assertion.value {
1856 let swift_val = json_to_swift(val);
1857 let field_is_optional = accessor_is_optional
1860 || assertion.field.as_deref().is_some_and(|f| {
1861 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1862 });
1863 let compare_expr = if field_is_optional {
1864 let cast_val = swift_numeric_literal_cast(&field_expr, "0");
1865 format!("({field_expr} ?? {cast_val})")
1866 } else {
1867 field_expr.clone()
1868 };
1869 let cast_swift_val = swift_numeric_literal_cast(&field_expr, &swift_val);
1870 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({compare_expr}, {cast_swift_val})");
1871 }
1872 }
1873 "less_than_or_equal" => {
1874 if let Some(val) = &assertion.value {
1875 let swift_val = json_to_swift(val);
1876 let field_is_optional = accessor_is_optional
1877 || assertion.field.as_deref().is_some_and(|f| {
1878 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1879 });
1880 let compare_expr = if field_is_optional {
1881 let cast_val = swift_numeric_literal_cast(&field_expr, "0");
1882 format!("({field_expr} ?? {cast_val})")
1883 } else {
1884 field_expr.clone()
1885 };
1886 let cast_swift_val = swift_numeric_literal_cast(&field_expr, &swift_val);
1887 let _ = writeln!(out, " XCTAssertLessThanOrEqual({compare_expr}, {cast_swift_val})");
1888 }
1889 }
1890 "starts_with" => {
1891 if let Some(expected) = &assertion.value {
1892 let swift_val = json_to_swift(expected);
1893 let _ = writeln!(
1894 out,
1895 " XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
1896 );
1897 }
1898 }
1899 "ends_with" => {
1900 if let Some(expected) = &assertion.value {
1901 let swift_val = json_to_swift(expected);
1902 let _ = writeln!(
1903 out,
1904 " XCTAssertTrue({string_expr}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
1905 );
1906 }
1907 }
1908 "min_length" => {
1909 if let Some(val) = &assertion.value {
1910 if let Some(n) = val.as_u64() {
1911 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({string_expr}.count, {n})");
1914 }
1915 }
1916 }
1917 "max_length" => {
1918 if let Some(val) = &assertion.value {
1919 if let Some(n) = val.as_u64() {
1920 let _ = writeln!(out, " XCTAssertLessThanOrEqual({string_expr}.count, {n})");
1921 }
1922 }
1923 }
1924 "count_min" => {
1925 if let Some(val) = &assertion.value {
1926 if let Some(n) = val.as_u64() {
1927 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1931 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({count_expr}, {n})");
1932 }
1933 }
1934 }
1935 "count_equals" => {
1936 if let Some(val) = &assertion.value {
1937 if let Some(n) = val.as_u64() {
1938 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1939 let _ = writeln!(out, " XCTAssertEqual({count_expr}, {n})");
1940 }
1941 }
1942 }
1943 "is_true" => {
1944 let _ = writeln!(out, " XCTAssertTrue({field_expr})");
1945 }
1946 "is_false" => {
1947 let _ = writeln!(out, " XCTAssertFalse({field_expr})");
1948 }
1949 "matches_regex" => {
1950 if let Some(expected) = &assertion.value {
1951 let swift_val = json_to_swift(expected);
1952 let _ = writeln!(
1953 out,
1954 " XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
1955 );
1956 }
1957 }
1958 "not_error" => {
1959 }
1961 "error" => {
1962 }
1964 "method_result" => {
1965 let _ = writeln!(out, " // method_result assertions not yet implemented for Swift");
1966 }
1967 other => {
1968 panic!("Swift e2e generator: unsupported assertion type: {other}");
1969 }
1970 }
1971}
1972
1973fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String, bool) {
1998 let Some(idx) = expr.find("()[") else {
1999 return (Vec::new(), expr.to_string(), false);
2000 };
2001 let after_open = idx + 3; let Some(close_rel) = expr[after_open..].find(']') else {
2003 return (Vec::new(), expr.to_string(), false);
2004 };
2005 let subscript_end = after_open + close_rel; let prefix = &expr[..idx + 2]; let subscript = &expr[idx + 2..=subscript_end]; let tail = &expr[subscript_end + 1..]; let method_dot = expr[..idx].rfind('.').unwrap_or(0);
2010 let method = &expr[method_dot + 1..idx];
2011 let local = format!("_vec_{}_{}", method, name_suffix);
2012
2013 let inner = subscript.trim_start_matches('[').trim_end_matches(']');
2018 let is_string_key = inner.starts_with('"') && inner.ends_with('"');
2019 let setup = if is_string_key {
2020 format!(
2021 "let {local} = (try? JSONSerialization.jsonObject(with: ({prefix}.toString() ?? \"{{}}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
2022 )
2023 } else {
2024 format!("let {local} = {prefix}")
2025 };
2026
2027 let rewritten = format!("{local}{subscript}{tail}");
2028 (vec![setup], rewritten, is_string_key)
2029}
2030
2031fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
2034 let resolved = field_resolver.resolve(field);
2035 let parts: Vec<&str> = resolved.split('.').collect();
2036
2037 let mut current_type: Option<String> = field_resolver.swift_root_type().cloned();
2042 let mut via_rust_vec = false;
2047
2048 let mut out = result_var.to_string();
2049 let mut has_optional = false;
2050 let mut path_so_far = String::new();
2051 let total = parts.len();
2052 for (i, part) in parts.iter().enumerate() {
2053 let is_leaf = i == total - 1;
2054 let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
2058 (&part[..bracket_pos], Some(&part[bracket_pos..]))
2059 } else {
2060 (part, None)
2061 };
2062
2063 if !path_so_far.is_empty() {
2064 path_so_far.push('.');
2065 }
2066 let base_path = {
2070 let mut p = path_so_far.clone();
2071 p.push_str(field_name);
2072 p
2073 };
2074 path_so_far.push_str(part);
2077
2078 let is_first_class = current_type.as_ref().map_or(false, |t| {
2084 field_resolver.swift_is_first_class(Some(t))
2085 });
2086 let property_syntax = !via_rust_vec && is_first_class;
2087 out.push('.');
2088 out.push_str(&field_name.to_lower_camel_case());
2091 if let Some(sub) = subscript {
2092 let field_is_optional = field_resolver.is_optional(&base_path);
2096 let access = if property_syntax { "" } else { "()" };
2097 if field_is_optional {
2098 out.push_str(&format!("{access}?"));
2099 has_optional = true;
2100 } else {
2101 out.push_str(access);
2102 }
2103 out.push_str(sub);
2104 current_type = field_resolver.swift_advance(current_type.as_deref(), field_name);
2114 if !property_syntax {
2115 via_rust_vec = true;
2116 }
2117 } else {
2118 if !property_syntax {
2119 out.push_str("()");
2120 }
2121 if !is_leaf && field_resolver.is_optional(&base_path) {
2124 out.push('?');
2125 has_optional = true;
2126 }
2127 current_type = field_resolver.swift_advance(current_type.as_deref(), field_name);
2128 }
2129 }
2130 (out, has_optional)
2131}
2132
2133#[allow(clippy::too_many_arguments)]
2155fn swift_traversal_contains_assert(
2156 array_part: &str,
2157 element_part: &str,
2158 full_field: &str,
2159 val_expr: &str,
2160 result_var: &str,
2161 negate: bool,
2162 msg: &str,
2163 enum_fields: &std::collections::HashSet<String>,
2164 field_resolver: &FieldResolver,
2165) -> String {
2166 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
2167 let resolved_full = field_resolver.resolve(full_field);
2168 let resolved_elem_part = resolved_full
2169 .find("[].")
2170 .map(|d| &resolved_full[d + 3..])
2171 .unwrap_or(element_part);
2172 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
2173 let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
2174 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
2175 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
2176 let elem_str = if elem_is_enum {
2177 format!("{elem_accessor}.toString()")
2180 } else if elem_is_optional {
2181 format!("({elem_accessor}?.toString() ?? \"\")")
2182 } else {
2183 format!("{elem_accessor}.toString()")
2184 };
2185 let assert_fn = if negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
2186 format!(" {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
2187}
2188
2189fn swift_array_contains_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
2190 let Some(f) = field else {
2191 return format!("{result_var}.map {{ $0.as_str().toString() }}");
2192 };
2193 let (accessor, _has_optional) = swift_build_accessor(f, result_var, field_resolver);
2194 format!("{accessor}?.map {{ $0.as_str().toString() }}")
2197}
2198
2199fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
2208 let Some(f) = field else {
2209 return format!("{result_var}.count");
2210 };
2211 let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
2212 if field_resolver.is_optional(f) {
2214 has_optional = true;
2215 }
2216 if has_optional {
2217 if accessor.contains("?.") {
2220 format!("{accessor}.count ?? 0")
2221 } else {
2222 format!("({accessor}?.count ?? 0)")
2225 }
2226 } else {
2227 format!("{accessor}.count")
2228 }
2229}
2230
2231fn json_to_swift(value: &serde_json::Value) -> String {
2233 match value {
2234 serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
2235 serde_json::Value::Bool(b) => b.to_string(),
2236 serde_json::Value::Number(n) => n.to_string(),
2237 serde_json::Value::Null => "nil".to_string(),
2238 serde_json::Value::Array(arr) => {
2239 let items: Vec<String> = arr.iter().map(json_to_swift).collect();
2240 format!("[{}]", items.join(", "))
2241 }
2242 serde_json::Value::Object(_) => {
2243 let json_str = serde_json::to_string(value).unwrap_or_default();
2244 format!("\"{}\"", escape_swift(&json_str))
2245 }
2246 }
2247}
2248
2249fn swift_numeric_literal_cast(field_expr: &str, numeric_literal: &str) -> String {
2257 if field_expr.contains("()") && !numeric_literal.contains('.') {
2260 format!("UInt({})", numeric_literal)
2261 } else {
2262 numeric_literal.to_string()
2263 }
2264}
2265
2266fn escape_swift(s: &str) -> String {
2268 escape_swift_str(s)
2269}
2270
2271fn swift_count_target(field_expr: &str, field_resolver: &FieldResolver, field: Option<&str>) -> String {
2288 let is_method_call = field_expr.trim_end().ends_with(')');
2289 if !is_method_call {
2290 return field_expr.to_string();
2291 }
2292 if let Some(f) = field
2293 && field_resolver.leaf_is_vec_via_swift_map(field_resolver.resolve(f))
2294 {
2295 return field_expr.to_string();
2296 }
2297 format!("{field_expr}.toString()")
2298}
2299
2300fn swift_call_result_type(call_config: &alef_core::config::e2e::CallConfig) -> Option<String> {
2314 const LOOKUP_LANGS: &[&str] = &["c", "csharp", "java", "kotlin", "go", "php"];
2315 for lang in LOOKUP_LANGS {
2316 if let Some(o) = call_config.overrides.get(*lang)
2317 && let Some(rt) = o.result_type.as_deref()
2318 && !rt.is_empty()
2319 {
2320 return Some(rt.to_string());
2321 }
2322 }
2323 None
2324}
2325
2326fn swift_first_class_field_supported(ty: &alef_core::ir::TypeRef, known_dto_names: &HashSet<String>) -> bool {
2340 use alef_core::ir::TypeRef;
2341 match ty {
2342 TypeRef::Primitive(_) | TypeRef::String => true,
2343 TypeRef::Named(name) => known_dto_names.contains(name),
2344 TypeRef::Vec(inner) | TypeRef::Optional(inner) => swift_first_class_field_supported(inner, known_dto_names),
2345 _ => false,
2346 }
2347}
2348
2349fn build_swift_first_class_map(
2369 type_defs: &[alef_core::ir::TypeDef],
2370 enum_defs: &[alef_core::ir::EnumDef],
2371 e2e_config: &crate::config::E2eConfig,
2372) -> SwiftFirstClassMap {
2373 use alef_core::ir::TypeRef;
2374 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2375 let mut vec_field_names: HashSet<String> = HashSet::new();
2376 fn inner_named(ty: &TypeRef) -> Option<String> {
2377 match ty {
2378 TypeRef::Named(n) => Some(n.clone()),
2379 TypeRef::Optional(inner) | TypeRef::Vec(inner) => inner_named(inner),
2380 _ => None,
2381 }
2382 }
2383 fn is_vec_ty(ty: &TypeRef) -> bool {
2384 match ty {
2385 TypeRef::Vec(_) => true,
2386 TypeRef::Optional(inner) => is_vec_ty(inner),
2387 _ => false,
2388 }
2389 }
2390 let mut known_dto_names: HashSet<String> = enum_defs
2393 .iter()
2394 .filter(|e| e.has_serde && e.variants.iter().all(|v| v.fields.is_empty()))
2395 .map(|e| e.name.clone())
2396 .collect();
2397
2398 let candidates: Vec<&alef_core::ir::TypeDef> = type_defs
2404 .iter()
2405 .filter(|td| !td.is_trait && !td.is_opaque && td.has_serde && !td.fields.is_empty())
2406 .collect();
2407
2408 loop {
2409 let prev = known_dto_names.len();
2410 for td in &candidates {
2411 if known_dto_names.contains(&td.name) {
2412 continue;
2413 }
2414 let all_supported = td
2415 .fields
2416 .iter()
2417 .filter(|f| !f.binding_excluded)
2418 .all(|f| swift_first_class_field_supported(&f.ty, &known_dto_names));
2419 if all_supported {
2420 known_dto_names.insert(td.name.clone());
2421 }
2422 }
2423 if known_dto_names.len() == prev {
2424 break;
2425 }
2426 }
2427
2428 let first_class_types: HashSet<String> = candidates
2433 .iter()
2434 .filter(|td| known_dto_names.contains(&td.name))
2435 .map(|td| td.name.clone())
2436 .collect();
2437
2438 for td in type_defs {
2439 let mut td_field_types: HashMap<String, String> = HashMap::new();
2440 for f in &td.fields {
2441 if let Some(named) = inner_named(&f.ty) {
2442 td_field_types.insert(f.name.clone(), named);
2443 }
2444 if is_vec_ty(&f.ty) {
2445 vec_field_names.insert(f.name.clone());
2446 }
2447 }
2448 if !td_field_types.is_empty() {
2449 field_types.insert(td.name.clone(), td_field_types);
2450 }
2451 }
2452 let root_type = if e2e_config.result_fields.is_empty() {
2456 None
2457 } else {
2458 let matches: Vec<&alef_core::ir::TypeDef> = type_defs
2459 .iter()
2460 .filter(|td| {
2461 let names: HashSet<&str> = td.fields.iter().map(|f| f.name.as_str()).collect();
2462 e2e_config.result_fields.iter().all(|rf| names.contains(rf.as_str()))
2463 })
2464 .collect();
2465 if matches.len() == 1 {
2466 Some(matches[0].name.clone())
2467 } else {
2468 None
2469 }
2470 };
2471 SwiftFirstClassMap {
2472 first_class_types,
2473 field_types,
2474 vec_field_names,
2475 root_type,
2476 }
2477}
2478
2479#[cfg(test)]
2480mod tests {
2481 use super::*;
2482 use crate::field_access::FieldResolver;
2483 use std::collections::{HashMap, HashSet};
2484
2485 fn make_resolver_tool_calls() -> FieldResolver {
2486 let mut optional = HashSet::new();
2490 optional.insert("choices.message.tool_calls".to_string());
2491 let mut arrays = HashSet::new();
2492 arrays.insert("choices".to_string());
2493 FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
2494 }
2495
2496 #[test]
2506 fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
2507 let resolver = make_resolver_tool_calls();
2508 let (accessor, has_optional) =
2509 swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
2510 assert!(
2513 accessor.contains("toolCalls?[0]"),
2514 "expected `toolCalls?[0]` for optional tool_calls, got: {accessor}"
2515 );
2516 assert!(
2518 !accessor.contains("?[0]?"),
2519 "must not emit trailing `?` after subscript index: {accessor}"
2520 );
2521 assert!(has_optional, "expected has_optional=true for optional field chain");
2523 assert!(
2525 accessor.contains("[0].function"),
2526 "expected `.function` (non-optional) after subscript: {accessor}"
2527 );
2528 }
2529}