1use crate::config::E2eConfig;
15use crate::escape::{escape_java as escape_swift_str, expand_fixture_templates, sanitize_filename, sanitize_ident};
16use crate::field_access::FieldResolver;
17use crate::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
18use alef_core::backend::GeneratedFile;
19use alef_core::config::ResolvedCrateConfig;
20use alef_core::hash::{self, CommentStyle};
21use alef_core::template_versions::toolchain;
22use anyhow::Result;
23use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
24use std::collections::HashSet;
25use std::fmt::Write as FmtWrite;
26use std::path::PathBuf;
27
28use super::E2eCodegen;
29use super::client;
30
31pub struct SwiftE2eCodegen;
33
34impl E2eCodegen for SwiftE2eCodegen {
35 fn generate(
36 &self,
37 groups: &[FixtureGroup],
38 e2e_config: &E2eConfig,
39 config: &ResolvedCrateConfig,
40 _type_defs: &[alef_core::ir::TypeDef],
41 _enums: &[alef_core::ir::EnumDef],
42 ) -> Result<Vec<GeneratedFile>> {
43 let lang = self.language_name();
44 let output_base = PathBuf::from(e2e_config.effective_output()).join("swift");
50
51 let mut files = Vec::new();
52
53 let call = &e2e_config.call;
55 let overrides = call.overrides.get(lang);
56 let function_name = overrides
57 .and_then(|o| o.function.as_ref())
58 .cloned()
59 .unwrap_or_else(|| call.function.clone());
60 let result_var = &call.result_var;
61 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
62
63 let swift_pkg = e2e_config.resolve_package("swift");
65 let pkg_name = swift_pkg
66 .as_ref()
67 .and_then(|p| p.name.as_ref())
68 .cloned()
69 .unwrap_or_else(|| config.name.to_upper_camel_case());
70 let pkg_path = swift_pkg
71 .as_ref()
72 .and_then(|p| p.path.as_ref())
73 .cloned()
74 .unwrap_or_else(|| "../../packages/swift".to_string());
75 let pkg_version = swift_pkg
76 .as_ref()
77 .and_then(|p| p.version.as_ref())
78 .cloned()
79 .or_else(|| config.resolved_version())
80 .unwrap_or_else(|| "0.1.0".to_string());
81
82 let module_name = pkg_name.as_str();
84
85 let registry_url = config
89 .try_github_repo()
90 .map(|repo| {
91 let base = repo.trim_end_matches('/').trim_end_matches(".git");
92 format!("{base}.git")
93 })
94 .unwrap_or_else(|_| format!("https://example.invalid/{module_name}.git"));
95
96 files.push(GeneratedFile {
99 path: output_base.join("Package.swift"),
100 content: render_package_swift(module_name, ®istry_url, &pkg_path, &pkg_version, e2e_config.dep_mode),
101 generated_header: false,
102 });
103
104 let tests_base = output_base.clone();
111
112 let field_resolver = FieldResolver::new(
113 &e2e_config.fields,
114 &e2e_config.fields_optional,
115 &e2e_config.result_fields,
116 &e2e_config.fields_array,
117 &e2e_config.fields_method_calls,
118 );
119
120 let client_factory: Option<&str> = overrides.and_then(|o| o.client_factory.as_deref());
122
123 for group in groups {
125 let active: Vec<&Fixture> = group
126 .fixtures
127 .iter()
128 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
129 .collect();
130
131 if active.is_empty() {
132 continue;
133 }
134
135 let class_name = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
136 let filename = format!("{class_name}.swift");
137 let content = render_test_file(
138 &group.category,
139 &active,
140 e2e_config,
141 module_name,
142 &class_name,
143 &function_name,
144 result_var,
145 &e2e_config.call.args,
146 &field_resolver,
147 result_is_simple,
148 &e2e_config.fields_enum,
149 client_factory,
150 );
151 files.push(GeneratedFile {
152 path: tests_base
153 .join("Tests")
154 .join(format!("{module_name}E2ETests"))
155 .join(filename),
156 content,
157 generated_header: true,
158 });
159 }
160
161 Ok(files)
162 }
163
164 fn language_name(&self) -> &'static str {
165 "swift"
166 }
167}
168
169fn render_package_swift(
174 module_name: &str,
175 registry_url: &str,
176 pkg_path: &str,
177 pkg_version: &str,
178 dep_mode: crate::config::DependencyMode,
179) -> String {
180 let min_macos = toolchain::SWIFT_MIN_MACOS;
181
182 let (dep_block, product_dep) = match dep_mode {
186 crate::config::DependencyMode::Registry => {
187 let dep = format!(r#" .package(url: "{registry_url}", from: "{pkg_version}")"#);
188 let pkg_id = registry_url
189 .trim_end_matches('/')
190 .trim_end_matches(".git")
191 .split('/')
192 .next_back()
193 .unwrap_or(module_name);
194 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
195 (dep, prod)
196 }
197 crate::config::DependencyMode::Local => {
198 let pkg_id = module_name;
204 let dep = format!(r#" .package(name: "{pkg_id}", path: "{pkg_path}")"#);
205 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
206 (dep, prod)
207 }
208 };
209 let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
212 let min_ios = toolchain::SWIFT_MIN_IOS;
213 let min_ios_major = min_ios.split('.').next().unwrap_or(min_ios);
214 format!(
218 r#"// swift-tools-version: 6.0
219import PackageDescription
220
221let package = Package(
222 name: "E2eSwift",
223 platforms: [
224 .macOS(.v{min_macos_major}),
225 .iOS(.v{min_ios_major}),
226 ],
227 dependencies: [
228{dep_block},
229 ],
230 targets: [
231 .testTarget(
232 name: "{module_name}E2ETests",
233 dependencies: [{product_dep}]
234 ),
235 ]
236)
237"#
238 )
239}
240
241#[allow(clippy::too_many_arguments)]
242fn render_test_file(
243 category: &str,
244 fixtures: &[&Fixture],
245 e2e_config: &E2eConfig,
246 module_name: &str,
247 class_name: &str,
248 function_name: &str,
249 result_var: &str,
250 args: &[crate::config::ArgMapping],
251 field_resolver: &FieldResolver,
252 result_is_simple: bool,
253 enum_fields: &HashSet<String>,
254 client_factory: Option<&str>,
255) -> String {
256 let needs_chdir = fixtures.iter().any(|f| {
263 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
264 call_config
265 .args
266 .iter()
267 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
268 });
269
270 let mut out = String::new();
271 out.push_str(&hash::header(CommentStyle::DoubleSlash));
272 let _ = writeln!(out, "import XCTest");
273 let _ = writeln!(out, "import Foundation");
274 let _ = writeln!(out, "import {module_name}");
275 let _ = writeln!(out, "import RustBridge");
276 let _ = writeln!(out);
277 let _ = writeln!(out, "/// E2e tests for category: {category}.");
278 let _ = writeln!(out, "final class {class_name}: XCTestCase {{");
279
280 if needs_chdir {
281 let _ = writeln!(out, " override class func setUp() {{");
289 let _ = writeln!(out, " super.setUp()");
290 let _ = writeln!(out, " let _testDocs = URL(fileURLWithPath: #filePath)");
291 let _ = writeln!(out, " .deletingLastPathComponent() // <Module>Tests/");
292 let _ = writeln!(out, " .deletingLastPathComponent() // Tests/");
293 let _ = writeln!(out, " .deletingLastPathComponent() // swift/");
294 let _ = writeln!(out, " .deletingLastPathComponent() // packages/");
295 let _ = writeln!(out, " .deletingLastPathComponent() // <repo root>");
296 let _ = writeln!(
297 out,
298 " .appendingPathComponent(\"{}\")",
299 e2e_config.test_documents_dir
300 );
301 let _ = writeln!(
302 out,
303 " if FileManager.default.fileExists(atPath: _testDocs.path) {{"
304 );
305 let _ = writeln!(
306 out,
307 " FileManager.default.changeCurrentDirectoryPath(_testDocs.path)"
308 );
309 let _ = writeln!(out, " }}");
310 let _ = writeln!(out, " }}");
311 let _ = writeln!(out);
312 }
313
314 for fixture in fixtures {
315 if fixture.is_http_test() {
316 render_http_test_method(&mut out, fixture);
317 } else {
318 render_test_method(
319 &mut out,
320 fixture,
321 e2e_config,
322 function_name,
323 result_var,
324 args,
325 field_resolver,
326 result_is_simple,
327 enum_fields,
328 client_factory,
329 );
330 }
331 let _ = writeln!(out);
332 }
333
334 let _ = writeln!(out, "}}");
335 out
336}
337
338struct SwiftTestClientRenderer;
345
346impl client::TestClientRenderer for SwiftTestClientRenderer {
347 fn language_name(&self) -> &'static str {
348 "swift"
349 }
350
351 fn sanitize_test_name(&self, id: &str) -> String {
352 sanitize_ident(id).to_upper_camel_case()
354 }
355
356 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
362 let _ = writeln!(out, " /// {description}");
363 let _ = writeln!(out, " func test{fn_name}() throws {{");
364 if let Some(reason) = skip_reason {
365 let escaped = escape_swift(reason);
366 let _ = writeln!(out, " try XCTSkipIf(true, \"{escaped}\")");
367 }
368 }
369
370 fn render_test_close(&self, out: &mut String) {
371 let _ = writeln!(out, " }}");
372 }
373
374 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
381 let method = ctx.method.to_uppercase();
382 let fixture_path = escape_swift(ctx.path);
383
384 let _ = writeln!(
385 out,
386 " let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
387 );
388 let _ = writeln!(
389 out,
390 " var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
391 );
392 let _ = writeln!(out, " _req.httpMethod = \"{method}\"");
393
394 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
396 header_pairs.sort_by_key(|(k, _)| k.as_str());
397 for (k, v) in &header_pairs {
398 let expanded_v = expand_fixture_templates(v);
399 let ek = escape_swift(k);
400 let ev = escape_swift(&expanded_v);
401 let _ = writeln!(out, " _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
402 }
403
404 if let Some(body) = ctx.body {
406 let json_str = serde_json::to_string(body).unwrap_or_default();
407 let escaped_body = escape_swift(&json_str);
408 let _ = writeln!(out, " _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
409 let _ = writeln!(
410 out,
411 " _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
412 );
413 }
414
415 let _ = writeln!(out, " var {}: HTTPURLResponse?", ctx.response_var);
416 let _ = writeln!(out, " var _responseData: Data?");
417 let _ = writeln!(out, " let _sema = DispatchSemaphore(value: 0)");
418 let _ = writeln!(
419 out,
420 " URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
421 );
422 let _ = writeln!(out, " {} = resp as? HTTPURLResponse", ctx.response_var);
423 let _ = writeln!(out, " _responseData = data");
424 let _ = writeln!(out, " _sema.signal()");
425 let _ = writeln!(out, " }}.resume()");
426 let _ = writeln!(out, " _sema.wait()");
427 let _ = writeln!(out, " let _resp = try XCTUnwrap({})", ctx.response_var);
428 }
429
430 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
431 let _ = writeln!(out, " XCTAssertEqual(_resp.statusCode, {status})");
432 }
433
434 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
435 let lower_name = name.to_lowercase();
436 let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
437 match expected {
438 "<<present>>" => {
439 let _ = writeln!(out, " XCTAssertNotNil({header_expr})");
440 }
441 "<<absent>>" => {
442 let _ = writeln!(out, " XCTAssertNil({header_expr})");
443 }
444 "<<uuid>>" => {
445 let _ = writeln!(out, " let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
446 let _ = writeln!(
447 out,
448 " 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))"
449 );
450 }
451 exact => {
452 let escaped = escape_swift(exact);
453 let _ = writeln!(out, " XCTAssertEqual({header_expr}, \"{escaped}\")");
454 }
455 }
456 }
457
458 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
459 if let serde_json::Value::String(s) = expected {
460 let escaped = escape_swift(s);
461 let _ = writeln!(
462 out,
463 " let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
464 );
465 let _ = writeln!(
466 out,
467 " XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
468 );
469 } else {
470 let json_str = serde_json::to_string(expected).unwrap_or_default();
471 let escaped = escape_swift(&json_str);
472 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
473 let _ = writeln!(
474 out,
475 " let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
476 );
477 let _ = writeln!(
478 out,
479 " let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
480 );
481 let _ = writeln!(
482 out,
483 " XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
484 );
485 }
486 }
487
488 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
489 if let Some(obj) = expected.as_object() {
490 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
491 let _ = writeln!(
492 out,
493 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
494 );
495 for (key, val) in obj {
496 let escaped_key = escape_swift(key);
497 let swift_val = json_to_swift(val);
498 let _ = writeln!(
499 out,
500 " XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
501 );
502 }
503 }
504 }
505
506 fn render_assert_validation_errors(
507 &self,
508 out: &mut String,
509 _response_var: &str,
510 errors: &[ValidationErrorExpectation],
511 ) {
512 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
513 let _ = writeln!(
514 out,
515 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
516 );
517 let _ = writeln!(
518 out,
519 " let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
520 );
521 for ve in errors {
522 let escaped_msg = escape_swift(&ve.msg);
523 let _ = writeln!(
524 out,
525 " XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
526 );
527 }
528 }
529}
530
531fn render_http_test_method(out: &mut String, fixture: &Fixture) {
536 let Some(http) = &fixture.http else {
537 return;
538 };
539
540 if http.expected_response.status_code == 101 {
542 let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
543 let description = fixture.description.replace('"', "\\\"");
544 let _ = writeln!(out, " /// {description}");
545 let _ = writeln!(out, " func test{method_name}() throws {{");
546 let _ = writeln!(
547 out,
548 " try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
549 );
550 let _ = writeln!(out, " }}");
551 return;
552 }
553
554 client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
555}
556
557#[allow(clippy::too_many_arguments)]
562fn render_test_method(
563 out: &mut String,
564 fixture: &Fixture,
565 e2e_config: &E2eConfig,
566 _function_name: &str,
567 _result_var: &str,
568 _args: &[crate::config::ArgMapping],
569 field_resolver: &FieldResolver,
570 result_is_simple: bool,
571 enum_fields: &HashSet<String>,
572 global_client_factory: Option<&str>,
573) {
574 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
576 let lang = "swift";
577 let call_overrides = call_config.overrides.get(lang);
578 let function_name = call_overrides
579 .and_then(|o| o.function.as_ref())
580 .cloned()
581 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
582 let client_factory: Option<&str> = call_overrides
584 .and_then(|o| o.client_factory.as_deref())
585 .or(global_client_factory);
586 let result_var = &call_config.result_var;
587 let args = &call_config.args;
588 let result_is_bytes_any_lang =
595 call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
596 eprintln!(
597 "[swift debug] fixture={} call={:?} result_is_bytes={} any_override_bytes={} overrides={}",
598 fixture.id,
599 fixture.call,
600 call_config.result_is_bytes,
601 call_config.overrides.values().any(|o| o.result_is_bytes),
602 call_config.overrides.len()
603 );
604 let result_is_simple = call_config.result_is_simple
605 || call_overrides.is_some_and(|o| o.result_is_simple)
606 || result_is_simple
607 || result_is_bytes_any_lang;
608 let result_is_array = call_config.result_is_array;
609 let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);
614
615 let method_name = fixture.id.to_upper_camel_case();
616 let description = &fixture.description;
617 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
618 let is_async = call_config.r#async;
619
620 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
622 let collect_snippet_opt = if is_streaming && !expects_error {
623 crate::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(lang, result_var, "chunks")
624 } else {
625 None
626 };
627 if is_streaming && !expects_error && collect_snippet_opt.is_none() {
634 if is_async {
635 let _ = writeln!(out, " func test{method_name}() async throws {{");
636 } else {
637 let _ = writeln!(out, " func test{method_name}() throws {{");
638 }
639 let _ = writeln!(out, " // {description}");
640 let _ = writeln!(
641 out,
642 " try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
643 fixture.id
644 );
645 let _ = writeln!(out, " }}");
646 return;
647 }
648 let collect_snippet = collect_snippet_opt.unwrap_or_default();
649
650 let has_unresolvable_json_object_arg = {
657 let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
658 options_via.is_none() && args.iter().any(|a| a.arg_type == "json_object" && a.name != "config")
659 };
660
661 if has_unresolvable_json_object_arg {
662 if is_async {
663 let _ = writeln!(out, " func test{method_name}() async throws {{");
664 } else {
665 let _ = writeln!(out, " func test{method_name}() throws {{");
666 }
667 let _ = writeln!(out, " // {description}");
668 let _ = writeln!(
669 out,
670 " try XCTSkipIf(true, \"swift: json_object request construction requires options_via configuration (fixture: {})\");",
671 fixture.id
672 );
673 let _ = writeln!(out, " }}");
674 return;
675 }
676
677 let mut visitor_setup_lines: Vec<String> = Vec::new();
681 let visitor_handle_expr: Option<String> = fixture
682 .visitor
683 .as_ref()
684 .map(|spec| super::swift_visitors::build_swift_visitor(&mut visitor_setup_lines, spec, &fixture.id));
685
686 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
690
691 let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
696 let per_call = call_overrides.map(|o| &o.enum_fields);
697 if let Some(pc) = per_call {
698 if !pc.is_empty() {
699 let mut merged = enum_fields.clone();
700 merged.extend(pc.keys().cloned());
701 std::borrow::Cow::Owned(merged)
702 } else {
703 std::borrow::Cow::Borrowed(enum_fields)
704 }
705 } else {
706 std::borrow::Cow::Borrowed(enum_fields)
707 }
708 };
709
710 let options_via_str: Option<&str> = call_overrides.and_then(|o| o.options_via.as_deref());
711 let options_type_str: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
712 let handle_config_fn_owned: Option<String> = call_config
716 .overrides
717 .get("c")
718 .and_then(|c| c.c_engine_factory.as_deref())
719 .map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
720 let (mut setup_lines, args_str) = build_args_and_setup(
721 &fixture.input,
722 args,
723 &fixture.id,
724 fixture.has_host_root_route(),
725 &function_name,
726 options_via_str,
727 options_type_str,
728 handle_config_fn_owned.as_deref(),
729 visitor_handle_expr.as_deref(),
730 );
731 if !visitor_setup_lines.is_empty() {
733 visitor_setup_lines.extend(setup_lines);
734 setup_lines = visitor_setup_lines;
735 }
736
737 let args_str = if extra_args.is_empty() {
739 args_str
740 } else if args_str.is_empty() {
741 extra_args.join(", ")
742 } else {
743 format!("{args_str}, {}", extra_args.join(", "))
744 };
745
746 let has_mock = fixture.mock_response.is_some();
751 let (call_setup, call_expr) = if let Some(_factory) = client_factory {
752 let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
753 let mock_url = if fixture.has_host_root_route() {
754 format!(
755 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\")",
756 fixture.id
757 )
758 } else {
759 format!(
760 "ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\"",
761 fixture.id
762 )
763 };
764 let client_constructor = if has_mock {
765 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
766 } else {
767 if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
769 format!(
770 "let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n \
771 let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n \
772 let _client = try DefaultClient(apiKey: _apiKey ?? \"test-key\", baseUrl: _baseUrl)"
773 )
774 } else {
775 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
776 }
777 };
778 let expr = if is_async {
779 format!("try await _client.{function_name}({args_str})")
780 } else {
781 format!("try _client.{function_name}({args_str})")
782 };
783 (Some(client_constructor), expr)
784 } else {
785 let expr = if is_async {
787 format!("try await {function_name}({args_str})")
788 } else {
789 format!("try {function_name}({args_str})")
790 };
791 (None, expr)
792 };
793 let _ = function_name;
795
796 if is_async {
797 let _ = writeln!(out, " func test{method_name}() async throws {{");
798 } else {
799 let _ = writeln!(out, " func test{method_name}() throws {{");
800 }
801 let _ = writeln!(out, " // {description}");
802
803 if expects_error {
804 if is_async {
808 let _ = writeln!(out, " do {{");
813 for line in &setup_lines {
814 let _ = writeln!(out, " {line}");
815 }
816 if let Some(setup) = &call_setup {
817 let _ = writeln!(out, " {setup}");
818 }
819 let _ = writeln!(out, " _ = {call_expr}");
820 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
821 let _ = writeln!(out, " }} catch {{");
822 let _ = writeln!(out, " // success");
823 let _ = writeln!(out, " }}");
824 } else {
825 let _ = writeln!(out, " do {{");
832 for line in &setup_lines {
833 let _ = writeln!(out, " {line}");
834 }
835 if let Some(setup) = &call_setup {
836 let _ = writeln!(out, " {setup}");
837 }
838 let _ = writeln!(out, " _ = {call_expr}");
839 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
840 let _ = writeln!(out, " }} catch {{");
841 let _ = writeln!(out, " // success");
842 let _ = writeln!(out, " }}");
843 }
844 let _ = writeln!(out, " }}");
845 return;
846 }
847
848 for line in &setup_lines {
849 let _ = writeln!(out, " {line}");
850 }
851
852 if let Some(setup) = &call_setup {
854 let _ = writeln!(out, " {setup}");
855 }
856
857 let _ = writeln!(out, " let {result_var} = {call_expr}");
858
859 if !collect_snippet.is_empty() {
862 for line in collect_snippet.lines() {
863 let _ = writeln!(out, " {line}");
864 }
865 }
866
867 for assertion in &fixture.assertions {
868 render_assertion(
869 out,
870 assertion,
871 result_var,
872 field_resolver,
873 result_is_simple,
874 result_is_array,
875 result_is_option,
876 &effective_enum_fields,
877 is_streaming,
878 );
879 }
880
881 let _ = writeln!(out, " }}");
882}
883
884#[allow(clippy::too_many_arguments)]
885fn build_args_and_setup(
899 input: &serde_json::Value,
900 args: &[crate::config::ArgMapping],
901 fixture_id: &str,
902 has_host_root_route: bool,
903 function_name: &str,
904 options_via: Option<&str>,
905 options_type: Option<&str>,
906 handle_config_fn: Option<&str>,
907 visitor_handle_expr: Option<&str>,
908) -> (Vec<String>, String) {
909 if args.is_empty() {
910 return (Vec::new(), String::new());
911 }
912
913 let mut setup_lines: Vec<String> = Vec::new();
914 let mut parts: Vec<String> = Vec::new();
915
916 let later_emits: Vec<bool> = (0..args.len())
921 .map(|i| {
922 args.iter().skip(i + 1).any(|a| {
923 let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
924 let v = input.get(f);
925 let has_value = matches!(v, Some(x) if !x.is_null());
926 has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
927 })
928 })
929 .collect();
930
931 for (idx, arg) in args.iter().enumerate() {
932 if arg.arg_type == "mock_url" {
933 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
934 let url_expr = if has_host_root_route {
935 format!(
936 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\")"
937 )
938 } else {
939 format!("ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"")
940 };
941 setup_lines.push(format!("let {} = {url_expr}", arg.name));
942 parts.push(arg.name.clone());
943 continue;
944 }
945
946 if arg.arg_type == "handle" {
947 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
948 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
949 let config_val = input.get(field);
950 let has_config = config_val
951 .is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
952 if has_config {
953 if let Some(from_json_fn) = handle_config_fn {
954 let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
955 let escaped = escape_swift_str(&json_str);
956 let config_var = format!("{}Config", arg.name.to_lower_camel_case());
957 setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
958 setup_lines.push(format!("let {var_name} = try createEngine({config_var})"));
959 } else {
960 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
961 }
962 } else {
963 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
964 }
965 parts.push(var_name);
966 continue;
967 }
968
969 if arg.arg_type == "bytes" {
974 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
975 let val = input.get(field);
976 match val {
977 None | Some(serde_json::Value::Null) if arg.optional => {
978 if later_emits[idx] {
979 parts.push("nil".to_string());
980 }
981 }
982 None | Some(serde_json::Value::Null) => {
983 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
984 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
985 parts.push(var_name);
986 }
987 Some(serde_json::Value::String(s)) => {
988 let escaped = escape_swift(s);
989 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
990 let data_var = format!("{}Data", arg.name.to_lower_camel_case());
991 setup_lines.push(format!(
992 "let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
993 ));
994 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
995 setup_lines.push(format!("for _byte in {data_var} {{ {var_name}.push(value: _byte) }}"));
996 parts.push(var_name);
997 }
998 Some(serde_json::Value::Array(arr)) => {
999 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1000 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1001 for v in arr {
1002 if let Some(n) = v.as_u64() {
1003 setup_lines.push(format!("{var_name}.push(value: UInt8({n}))"));
1004 }
1005 }
1006 parts.push(var_name);
1007 }
1008 Some(other) => {
1009 let json_str = serde_json::to_string(other).unwrap_or_default();
1011 let escaped = escape_swift(&json_str);
1012 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1013 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1014 setup_lines.push(format!(
1015 "for _byte in Array(\"{escaped}\".utf8) {{ {var_name}.push(value: _byte) }}"
1016 ));
1017 parts.push(var_name);
1018 }
1019 }
1020 continue;
1021 }
1022
1023 let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
1029 let is_batch_fn = function_name.starts_with("batch") || function_name.starts_with("Batch");
1030 if is_config_arg && !is_batch_fn {
1031 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1032 let val = input.get(field);
1033 let json_str = match val {
1034 None | Some(serde_json::Value::Null) => "{}".to_string(),
1035 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1036 };
1037 let escaped = escape_swift(&json_str);
1038 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1039 let from_json_fn = if let Some(type_name) = options_type {
1041 format!("{}FromJson", type_name.to_lower_camel_case())
1042 } else {
1043 "extractionConfigFromJson".to_string()
1044 };
1045 setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1046 parts.push(var_name);
1047 continue;
1048 }
1049
1050 if arg.arg_type == "json_object" && options_via == Some("from_json") {
1057 if let Some(type_name) = options_type {
1058 let resolved_val = super::resolve_field(input, &arg.field);
1059 let json_str = match resolved_val {
1060 serde_json::Value::Null => "{}".to_string(),
1061 v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1062 };
1063 let escaped = escape_swift(&json_str);
1064 let var_name = format!("_{}", arg.name.to_lower_camel_case());
1065 if let Some(handle_expr) = visitor_handle_expr {
1066 let with_visitor_fn = format!("{}FromJsonWithVisitor", type_name.to_lower_camel_case());
1071 let handle_var = format!("_visitorHandle_{}", var_name.trim_start_matches('_'));
1072 setup_lines.push(format!("let {handle_var} = {handle_expr}"));
1073 setup_lines.push(format!(
1074 "let {var_name} = try {with_visitor_fn}(\"{escaped}\", {handle_var})"
1075 ));
1076 } else {
1077 let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
1078 setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1079 }
1080 parts.push(var_name);
1081 continue;
1082 }
1083 }
1084
1085 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1086 let val = input.get(field);
1087 match val {
1088 None | Some(serde_json::Value::Null) if arg.optional => {
1089 if later_emits[idx] {
1093 parts.push("nil".to_string());
1094 }
1095 }
1096 None | Some(serde_json::Value::Null) => {
1097 let default_val = match arg.arg_type.as_str() {
1098 "string" => "\"\"".to_string(),
1099 "int" | "integer" => "0".to_string(),
1100 "float" | "number" => "0.0".to_string(),
1101 "bool" | "boolean" => "false".to_string(),
1102 _ => "nil".to_string(),
1103 };
1104 parts.push(default_val);
1105 }
1106 Some(v) => {
1107 parts.push(json_to_swift(v));
1108 }
1109 }
1110 }
1111
1112 (setup_lines, parts.join(", "))
1113}
1114
1115#[allow(clippy::too_many_arguments)]
1116fn render_assertion(
1117 out: &mut String,
1118 assertion: &Assertion,
1119 result_var: &str,
1120 field_resolver: &FieldResolver,
1121 result_is_simple: bool,
1122 result_is_array: bool,
1123 result_is_option: bool,
1124 enum_fields: &HashSet<String>,
1125 is_streaming: bool,
1126) {
1127 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1132 if let Some(f) = &assertion.field {
1137 let is_streaming_usage_path =
1138 is_streaming && (f == "usage" || (f.starts_with("usage.") || f.starts_with("usage[")));
1139 if !f.is_empty()
1140 && (crate::codegen::streaming_assertions::is_streaming_virtual_field(f) || is_streaming_usage_path)
1141 {
1142 if let Some(expr) =
1143 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "swift", "chunks")
1144 {
1145 let line = match assertion.assertion_type.as_str() {
1146 "count_min" => {
1147 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1148 format!(" XCTAssertGreaterThanOrEqual(chunks.count, {n})\n")
1149 } else {
1150 String::new()
1151 }
1152 }
1153 "count_equals" => {
1154 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1155 format!(" XCTAssertEqual(chunks.count, {n})\n")
1156 } else {
1157 String::new()
1158 }
1159 }
1160 "equals" => {
1161 if let Some(serde_json::Value::String(s)) = &assertion.value {
1162 let escaped = escape_swift(s);
1163 format!(" XCTAssertEqual({expr}, \"{escaped}\")\n")
1164 } else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
1165 format!(" XCTAssertEqual({expr}, {b})\n")
1166 } else {
1167 String::new()
1168 }
1169 }
1170 "not_empty" => {
1171 format!(" XCTAssertFalse({expr}.isEmpty, \"expected non-empty\")\n")
1172 }
1173 "is_empty" => {
1174 format!(" XCTAssertTrue({expr}.isEmpty, \"expected empty\")\n")
1175 }
1176 "is_true" => {
1177 format!(" XCTAssertTrue({expr})\n")
1178 }
1179 "is_false" => {
1180 format!(" XCTAssertFalse({expr})\n")
1181 }
1182 "greater_than" => {
1183 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1184 format!(" XCTAssertGreaterThan(chunks.count, {n})\n")
1185 } else {
1186 String::new()
1187 }
1188 }
1189 "contains" => {
1190 if let Some(serde_json::Value::String(s)) = &assertion.value {
1191 let escaped = escape_swift(s);
1192 format!(
1193 " XCTAssertTrue({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\")\n"
1194 )
1195 } else {
1196 String::new()
1197 }
1198 }
1199 _ => format!(
1200 " // streaming field '{f}': assertion type '{}' not rendered\n",
1201 assertion.assertion_type
1202 ),
1203 };
1204 if !line.is_empty() {
1205 out.push_str(&line);
1206 }
1207 }
1208 return;
1209 }
1210 }
1211
1212 if let Some(f) = &assertion.field {
1214 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1215 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1216 return;
1217 }
1218 }
1219
1220 if let Some(f) = &assertion.field {
1225 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1226 let _ = writeln!(
1227 out,
1228 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Swift)"
1229 );
1230 return;
1231 }
1232 }
1233
1234 let field_is_enum = assertion
1236 .field
1237 .as_deref()
1238 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1239
1240 let field_is_optional = assertion.field.as_deref().is_some_and(|f| {
1241 !f.is_empty() && (field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f)))
1242 });
1243 let field_is_array = assertion.field.as_deref().is_some_and(|f| {
1244 !f.is_empty()
1245 && (field_resolver.is_array(f)
1246 || field_resolver.is_array(field_resolver.resolve(f))
1247 || field_resolver.is_collection_root(f)
1248 || field_resolver.is_collection_root(field_resolver.resolve(f)))
1249 });
1250
1251 let field_expr_raw = if result_is_simple {
1252 result_var.to_string()
1253 } else {
1254 match &assertion.field {
1255 Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
1256 _ => result_var.to_string(),
1257 }
1258 };
1259
1260 let local_suffix = {
1270 use std::hash::{Hash, Hasher};
1271 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1272 assertion.field.hash(&mut hasher);
1273 assertion
1274 .value
1275 .as_ref()
1276 .map(|v| v.to_string())
1277 .unwrap_or_default()
1278 .hash(&mut hasher);
1279 format!(
1280 "{}_{:x}",
1281 assertion.assertion_type.replace(['-', '.'], "_"),
1282 hasher.finish() & 0xffff_ffff,
1283 )
1284 };
1285 let (vec_setup, field_expr, is_map_subscript) = materialise_vec_temporaries(&field_expr_raw, &local_suffix);
1286 let field_uses_traversal = assertion.field.as_deref().is_some_and(|f| f.contains("[]."));
1291 let traversal_skips_field_expr = field_uses_traversal
1292 && matches!(
1293 assertion.assertion_type.as_str(),
1294 "contains" | "not_contains" | "not_empty" | "is_empty"
1295 );
1296 if !traversal_skips_field_expr {
1297 for line in &vec_setup {
1298 let _ = writeln!(out, " {line}");
1299 }
1300 }
1301
1302 let accessor_is_optional = field_expr.contains("?.");
1308
1309 let string_expr = if is_map_subscript {
1318 format!("({field_expr} ?? \"\")")
1322 } else if field_is_enum && (field_is_optional || accessor_is_optional) {
1323 format!("({field_expr}?.toString() ?? \"\")")
1326 } else if field_is_enum {
1327 format!("{field_expr}.toString()")
1332 } else if field_is_optional {
1333 format!("({field_expr}?.toString() ?? \"\")")
1335 } else if accessor_is_optional {
1336 format!("({field_expr}.toString() ?? \"\")")
1339 } else {
1340 format!("{field_expr}.toString()")
1341 };
1342
1343 match assertion.assertion_type.as_str() {
1344 "equals" => {
1345 if let Some(expected) = &assertion.value {
1346 let swift_val = json_to_swift(expected);
1347 if expected.is_string() {
1348 if field_is_enum {
1349 let trim_expr =
1353 format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1354 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1355 } else {
1356 let trim_expr =
1361 format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1362 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1363 }
1364 } else {
1365 let _ = writeln!(out, " XCTAssertEqual({field_expr}, {swift_val})");
1366 }
1367 }
1368 }
1369 "contains" => {
1370 if let Some(expected) = &assertion.value {
1371 let swift_val = json_to_swift(expected);
1372 let no_field = assertion.field.as_deref().is_none_or(|f| f.is_empty());
1375 if result_is_simple && result_is_array && no_field {
1376 let _ = writeln!(
1379 out,
1380 " XCTAssertTrue({result_var}.map {{ $0.as_str().toString() }}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1381 );
1382 } else {
1383 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1385 if let Some(dot) = f.find("[].") {
1386 let array_part = &f[..dot];
1387 let elem_part = &f[dot + 3..];
1388 let line = swift_traversal_contains_assert(
1389 array_part,
1390 elem_part,
1391 f,
1392 &swift_val,
1393 result_var,
1394 false,
1395 &format!("expected to contain: \\({swift_val})"),
1396 enum_fields,
1397 field_resolver,
1398 );
1399 let _ = writeln!(out, "{line}");
1400 true
1401 } else {
1402 false
1403 }
1404 } else {
1405 false
1406 };
1407 if !traversal_handled {
1408 let field_is_array = assertion
1410 .field
1411 .as_deref()
1412 .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
1413 if field_is_array {
1414 let contains_expr =
1415 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1416 let _ = writeln!(
1417 out,
1418 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1419 );
1420 } else if field_is_enum {
1421 let _ = writeln!(
1424 out,
1425 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1426 );
1427 } else {
1428 let _ = writeln!(
1429 out,
1430 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1431 );
1432 }
1433 }
1434 }
1435 }
1436 }
1437 "contains_all" => {
1438 if let Some(values) = &assertion.values {
1439 if let Some(f) = assertion.field.as_deref() {
1441 if let Some(dot) = f.find("[].") {
1442 let array_part = &f[..dot];
1443 let elem_part = &f[dot + 3..];
1444 for val in values {
1445 let swift_val = json_to_swift(val);
1446 let line = swift_traversal_contains_assert(
1447 array_part,
1448 elem_part,
1449 f,
1450 &swift_val,
1451 result_var,
1452 false,
1453 &format!("expected to contain: \\({swift_val})"),
1454 enum_fields,
1455 field_resolver,
1456 );
1457 let _ = writeln!(out, "{line}");
1458 }
1459 } else {
1461 let field_is_array = field_resolver.is_array(field_resolver.resolve(f));
1463 if field_is_array {
1464 let contains_expr =
1465 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1466 for val in values {
1467 let swift_val = json_to_swift(val);
1468 let _ = writeln!(
1469 out,
1470 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1471 );
1472 }
1473 } else if field_is_enum {
1474 for val in values {
1477 let swift_val = json_to_swift(val);
1478 let _ = writeln!(
1479 out,
1480 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1481 );
1482 }
1483 } else {
1484 for val in values {
1485 let swift_val = json_to_swift(val);
1486 let _ = writeln!(
1487 out,
1488 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1489 );
1490 }
1491 }
1492 }
1493 } else {
1494 for val in values {
1496 let swift_val = json_to_swift(val);
1497 let _ = writeln!(
1498 out,
1499 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1500 );
1501 }
1502 }
1503 }
1504 }
1505 "not_contains" => {
1506 if let Some(expected) = &assertion.value {
1507 let swift_val = json_to_swift(expected);
1508 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1510 if let Some(dot) = f.find("[].") {
1511 let array_part = &f[..dot];
1512 let elem_part = &f[dot + 3..];
1513 let line = swift_traversal_contains_assert(
1514 array_part,
1515 elem_part,
1516 f,
1517 &swift_val,
1518 result_var,
1519 true,
1520 &format!("expected NOT to contain: \\({swift_val})"),
1521 enum_fields,
1522 field_resolver,
1523 );
1524 let _ = writeln!(out, "{line}");
1525 true
1526 } else {
1527 false
1528 }
1529 } else {
1530 false
1531 };
1532 if !traversal_handled {
1533 let _ = writeln!(
1534 out,
1535 " XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
1536 );
1537 }
1538 }
1539 }
1540 "not_empty" => {
1541 let traversal_not_empty_handled = if let Some(f) = assertion.field.as_deref() {
1548 if let Some(dot) = f.find("[].") {
1549 let array_part = &f[..dot];
1550 let elem_part = &f[dot + 3..];
1551 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1552 let resolved_full = field_resolver.resolve(f);
1553 let resolved_elem_part = resolved_full
1554 .find("[].")
1555 .map(|d| &resolved_full[d + 3..])
1556 .unwrap_or(elem_part);
1557 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1558 let elem_is_enum = enum_fields.contains(f) || enum_fields.contains(resolved_full);
1559 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1560 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1561 let elem_str = if elem_is_enum {
1562 format!("{elem_accessor}.to_string().toString()")
1563 } else if elem_is_optional {
1564 format!("({elem_accessor}?.toString() ?? \"\")")
1565 } else {
1566 format!("{elem_accessor}.toString()")
1567 };
1568 let _ = writeln!(
1569 out,
1570 " XCTAssertTrue({array_accessor}.contains(where: {{ !{elem_str}.isEmpty }}), \"expected non-empty value\")"
1571 );
1572 true
1573 } else {
1574 false
1575 }
1576 } else {
1577 false
1578 };
1579 if !traversal_not_empty_handled {
1580 if bare_result_is_option {
1581 let _ = writeln!(out, " XCTAssertNotNil({result_var}, \"expected non-nil value\")");
1582 } else if field_is_optional {
1583 let _ = writeln!(out, " XCTAssertNotNil({field_expr}, \"expected non-nil value\")");
1584 } else if field_is_array {
1585 let _ = writeln!(
1586 out,
1587 " XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
1588 );
1589 } else if result_is_simple {
1590 let _ = writeln!(
1592 out,
1593 " XCTAssertFalse({result_var}.isEmpty, \"expected non-empty value\")"
1594 );
1595 } else {
1596 let len_expr = if accessor_is_optional {
1605 format!("({field_expr}.len() ?? 0)")
1606 } else {
1607 format!("{field_expr}.len()")
1608 };
1609 let _ = writeln!(
1610 out,
1611 " XCTAssertGreaterThan({len_expr}, 0, \"expected non-empty value\")"
1612 );
1613 }
1614 }
1615 }
1616 "is_empty" => {
1617 if bare_result_is_option {
1618 let _ = writeln!(out, " XCTAssertNil({result_var}, \"expected nil value\")");
1619 } else if field_is_optional {
1620 let _ = writeln!(out, " XCTAssertNil({field_expr}, \"expected nil value\")");
1621 } else if field_is_array {
1622 let _ = writeln!(
1623 out,
1624 " XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
1625 );
1626 } else {
1627 let len_expr = if accessor_is_optional {
1631 format!("({field_expr}.len() ?? 0)")
1632 } else {
1633 format!("{field_expr}.len()")
1634 };
1635 let _ = writeln!(out, " XCTAssertEqual({len_expr}, 0, \"expected empty value\")");
1636 }
1637 }
1638 "contains_any" => {
1639 if let Some(values) = &assertion.values {
1640 let checks: Vec<String> = values
1641 .iter()
1642 .map(|v| {
1643 let swift_val = json_to_swift(v);
1644 format!("{string_expr}.contains({swift_val})")
1645 })
1646 .collect();
1647 let joined = checks.join(" || ");
1648 let _ = writeln!(
1649 out,
1650 " XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
1651 );
1652 }
1653 }
1654 "greater_than" => {
1655 if let Some(val) = &assertion.value {
1656 let swift_val = json_to_swift(val);
1657 let field_is_optional = accessor_is_optional
1660 || assertion.field.as_deref().is_some_and(|f| {
1661 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1662 });
1663 let compare_expr = if field_is_optional {
1664 format!("({field_expr} ?? 0)")
1665 } else {
1666 field_expr.clone()
1667 };
1668 let _ = writeln!(out, " XCTAssertGreaterThan({compare_expr}, {swift_val})");
1669 }
1670 }
1671 "less_than" => {
1672 if let Some(val) = &assertion.value {
1673 let swift_val = json_to_swift(val);
1674 let field_is_optional = accessor_is_optional
1675 || assertion.field.as_deref().is_some_and(|f| {
1676 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1677 });
1678 let compare_expr = if field_is_optional {
1679 format!("({field_expr} ?? 0)")
1680 } else {
1681 field_expr.clone()
1682 };
1683 let _ = writeln!(out, " XCTAssertLessThan({compare_expr}, {swift_val})");
1684 }
1685 }
1686 "greater_than_or_equal" => {
1687 if let Some(val) = &assertion.value {
1688 let swift_val = json_to_swift(val);
1689 let field_is_optional = accessor_is_optional
1692 || assertion.field.as_deref().is_some_and(|f| {
1693 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1694 });
1695 let compare_expr = if field_is_optional {
1696 format!("({field_expr} ?? 0)")
1697 } else {
1698 field_expr.clone()
1699 };
1700 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({compare_expr}, {swift_val})");
1701 }
1702 }
1703 "less_than_or_equal" => {
1704 if let Some(val) = &assertion.value {
1705 let swift_val = json_to_swift(val);
1706 let field_is_optional = accessor_is_optional
1707 || assertion.field.as_deref().is_some_and(|f| {
1708 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1709 });
1710 let compare_expr = if field_is_optional {
1711 format!("({field_expr} ?? 0)")
1712 } else {
1713 field_expr.clone()
1714 };
1715 let _ = writeln!(out, " XCTAssertLessThanOrEqual({compare_expr}, {swift_val})");
1716 }
1717 }
1718 "starts_with" => {
1719 if let Some(expected) = &assertion.value {
1720 let swift_val = json_to_swift(expected);
1721 let _ = writeln!(
1722 out,
1723 " XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
1724 );
1725 }
1726 }
1727 "ends_with" => {
1728 if let Some(expected) = &assertion.value {
1729 let swift_val = json_to_swift(expected);
1730 let _ = writeln!(
1731 out,
1732 " XCTAssertTrue({string_expr}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
1733 );
1734 }
1735 }
1736 "min_length" => {
1737 if let Some(val) = &assertion.value {
1738 if let Some(n) = val.as_u64() {
1739 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({string_expr}.count, {n})");
1742 }
1743 }
1744 }
1745 "max_length" => {
1746 if let Some(val) = &assertion.value {
1747 if let Some(n) = val.as_u64() {
1748 let _ = writeln!(out, " XCTAssertLessThanOrEqual({string_expr}.count, {n})");
1749 }
1750 }
1751 }
1752 "count_min" => {
1753 if let Some(val) = &assertion.value {
1754 if let Some(n) = val.as_u64() {
1755 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1759 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({count_expr}, {n})");
1760 }
1761 }
1762 }
1763 "count_equals" => {
1764 if let Some(val) = &assertion.value {
1765 if let Some(n) = val.as_u64() {
1766 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1767 let _ = writeln!(out, " XCTAssertEqual({count_expr}, {n})");
1768 }
1769 }
1770 }
1771 "is_true" => {
1772 let _ = writeln!(out, " XCTAssertTrue({field_expr})");
1773 }
1774 "is_false" => {
1775 let _ = writeln!(out, " XCTAssertFalse({field_expr})");
1776 }
1777 "matches_regex" => {
1778 if let Some(expected) = &assertion.value {
1779 let swift_val = json_to_swift(expected);
1780 let _ = writeln!(
1781 out,
1782 " XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
1783 );
1784 }
1785 }
1786 "not_error" => {
1787 }
1789 "error" => {
1790 }
1792 "method_result" => {
1793 let _ = writeln!(out, " // method_result assertions not yet implemented for Swift");
1794 }
1795 other => {
1796 panic!("Swift e2e generator: unsupported assertion type: {other}");
1797 }
1798 }
1799}
1800
1801fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String, bool) {
1826 let Some(idx) = expr.find("()[") else {
1827 return (Vec::new(), expr.to_string(), false);
1828 };
1829 let after_open = idx + 3; let Some(close_rel) = expr[after_open..].find(']') else {
1831 return (Vec::new(), expr.to_string(), false);
1832 };
1833 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);
1838 let method = &expr[method_dot + 1..idx];
1839 let local = format!("_vec_{}_{}", method, name_suffix);
1840
1841 let inner = subscript.trim_start_matches('[').trim_end_matches(']');
1846 let is_string_key = inner.starts_with('"') && inner.ends_with('"');
1847 let setup = if is_string_key {
1848 format!(
1849 "let {local} = (try? JSONSerialization.jsonObject(with: ({prefix}.toString() ?? \"{{}}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
1850 )
1851 } else {
1852 format!("let {local} = {prefix}")
1853 };
1854
1855 let rewritten = format!("{local}{subscript}{tail}");
1856 (vec![setup], rewritten, is_string_key)
1857}
1858
1859fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
1862 let resolved = field_resolver.resolve(field);
1863 let parts: Vec<&str> = resolved.split('.').collect();
1864
1865 let mut out = result_var.to_string();
1868 let mut has_optional = false;
1869 let mut path_so_far = String::new();
1870 let total = parts.len();
1871 for (i, part) in parts.iter().enumerate() {
1872 let is_leaf = i == total - 1;
1873 let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
1877 (&part[..bracket_pos], Some(&part[bracket_pos..]))
1878 } else {
1879 (part, None)
1880 };
1881
1882 if !path_so_far.is_empty() {
1883 path_so_far.push('.');
1884 }
1885 let base_path = {
1889 let mut p = path_so_far.clone();
1890 p.push_str(field_name);
1891 p
1892 };
1893 path_so_far.push_str(part);
1896
1897 out.push('.');
1898 out.push_str(field_name);
1899 if let Some(sub) = subscript {
1900 let field_is_optional = field_resolver.is_optional(&base_path);
1904 if field_is_optional {
1905 out.push_str("()?");
1906 has_optional = true;
1907 } else {
1908 out.push_str("()");
1909 }
1910 out.push_str(sub);
1911 } else {
1921 out.push_str("()");
1922 if !is_leaf && field_resolver.is_optional(&base_path) {
1925 out.push('?');
1926 has_optional = true;
1927 }
1928 }
1929 }
1930 (out, has_optional)
1931}
1932
1933#[allow(clippy::too_many_arguments)]
1955fn swift_traversal_contains_assert(
1956 array_part: &str,
1957 element_part: &str,
1958 full_field: &str,
1959 val_expr: &str,
1960 result_var: &str,
1961 negate: bool,
1962 msg: &str,
1963 enum_fields: &std::collections::HashSet<String>,
1964 field_resolver: &FieldResolver,
1965) -> String {
1966 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1967 let resolved_full = field_resolver.resolve(full_field);
1968 let resolved_elem_part = resolved_full
1969 .find("[].")
1970 .map(|d| &resolved_full[d + 3..])
1971 .unwrap_or(element_part);
1972 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1973 let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
1974 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1975 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1976 let elem_str = if elem_is_enum {
1977 format!("{elem_accessor}.toString()")
1980 } else if elem_is_optional {
1981 format!("({elem_accessor}?.toString() ?? \"\")")
1982 } else {
1983 format!("{elem_accessor}.toString()")
1984 };
1985 let assert_fn = if negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
1986 format!(" {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
1987}
1988
1989fn swift_array_contains_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
1990 let Some(f) = field else {
1991 return format!("{result_var}.map {{ $0.as_str().toString() }}");
1992 };
1993 let (accessor, _has_optional) = swift_build_accessor(f, result_var, field_resolver);
1994 format!("{accessor}?.map {{ $0.as_str().toString() }}")
1997}
1998
1999fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
2008 let Some(f) = field else {
2009 return format!("{result_var}.count");
2010 };
2011 let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
2012 if field_resolver.is_optional(f) {
2014 has_optional = true;
2015 }
2016 if has_optional {
2017 if accessor.contains("?.") {
2020 format!("{accessor}.count ?? 0")
2021 } else {
2022 format!("({accessor}?.count ?? 0)")
2025 }
2026 } else {
2027 format!("{accessor}.count")
2028 }
2029}
2030
2031fn json_to_swift(value: &serde_json::Value) -> String {
2033 match value {
2034 serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
2035 serde_json::Value::Bool(b) => b.to_string(),
2036 serde_json::Value::Number(n) => n.to_string(),
2037 serde_json::Value::Null => "nil".to_string(),
2038 serde_json::Value::Array(arr) => {
2039 let items: Vec<String> = arr.iter().map(json_to_swift).collect();
2040 format!("[{}]", items.join(", "))
2041 }
2042 serde_json::Value::Object(_) => {
2043 let json_str = serde_json::to_string(value).unwrap_or_default();
2044 format!("\"{}\"", escape_swift(&json_str))
2045 }
2046 }
2047}
2048
2049fn escape_swift(s: &str) -> String {
2051 escape_swift_str(s)
2052}
2053
2054#[cfg(test)]
2055mod tests {
2056 use super::*;
2057 use crate::field_access::FieldResolver;
2058 use std::collections::{HashMap, HashSet};
2059
2060 fn make_resolver_tool_calls() -> FieldResolver {
2061 let mut optional = HashSet::new();
2065 optional.insert("choices.message.tool_calls".to_string());
2066 let mut arrays = HashSet::new();
2067 arrays.insert("choices".to_string());
2068 FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
2069 }
2070
2071 #[test]
2078 fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
2079 let resolver = make_resolver_tool_calls();
2080 let (accessor, has_optional) =
2083 swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
2084 assert!(
2087 accessor.contains("tool_calls()?[0]"),
2088 "expected `tool_calls()?[0]` for optional tool_calls, got: {accessor}"
2089 );
2090 assert!(
2092 !accessor.contains("?[0]?"),
2093 "must not emit trailing `?` after subscript index: {accessor}"
2094 );
2095 assert!(has_optional, "expected has_optional=true for optional field chain");
2097 assert!(
2099 accessor.contains("[0].function()"),
2100 "expected `.function()` (non-optional) after subscript: {accessor}"
2101 );
2102 }
2103}