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