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 ) -> Result<Vec<GeneratedFile>> {
42 let lang = self.language_name();
43 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
44
45 let mut files = Vec::new();
46
47 let call = &e2e_config.call;
49 let overrides = call.overrides.get(lang);
50 let function_name = overrides
51 .and_then(|o| o.function.as_ref())
52 .cloned()
53 .unwrap_or_else(|| call.function.clone());
54 let result_var = &call.result_var;
55 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
56
57 let swift_pkg = e2e_config.resolve_package("swift");
59 let pkg_name = swift_pkg
60 .as_ref()
61 .and_then(|p| p.name.as_ref())
62 .cloned()
63 .unwrap_or_else(|| config.name.to_upper_camel_case());
64 let pkg_path = swift_pkg
65 .as_ref()
66 .and_then(|p| p.path.as_ref())
67 .cloned()
68 .unwrap_or_else(|| "../../packages/swift".to_string());
69 let pkg_version = swift_pkg
70 .as_ref()
71 .and_then(|p| p.version.as_ref())
72 .cloned()
73 .or_else(|| config.resolved_version())
74 .unwrap_or_else(|| "0.1.0".to_string());
75
76 let module_name = pkg_name.as_str();
78
79 let registry_url = config
83 .try_github_repo()
84 .map(|repo| {
85 let base = repo.trim_end_matches('/').trim_end_matches(".git");
86 format!("{base}.git")
87 })
88 .unwrap_or_else(|_| format!("https://example.invalid/{module_name}.git"));
89
90 files.push(GeneratedFile {
93 path: output_base.join("Package.swift"),
94 content: render_package_swift(module_name, ®istry_url, &pkg_path, &pkg_version, e2e_config.dep_mode),
95 generated_header: false,
96 });
97
98 let tests_base = output_base.clone();
104
105 let field_resolver = FieldResolver::new(
106 &e2e_config.fields,
107 &e2e_config.fields_optional,
108 &e2e_config.result_fields,
109 &e2e_config.fields_array,
110 &e2e_config.fields_method_calls,
111 );
112
113 let client_factory: Option<&str> = overrides.and_then(|o| o.client_factory.as_deref());
115
116 for group in groups {
118 let active: Vec<&Fixture> = group
119 .fixtures
120 .iter()
121 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
122 .collect();
123
124 if active.is_empty() {
125 continue;
126 }
127
128 let class_name = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
129 let filename = format!("{class_name}.swift");
130 let content = render_test_file(
131 &group.category,
132 &active,
133 e2e_config,
134 module_name,
135 &class_name,
136 &function_name,
137 result_var,
138 &e2e_config.call.args,
139 &field_resolver,
140 result_is_simple,
141 &e2e_config.fields_enum,
142 client_factory,
143 );
144 files.push(GeneratedFile {
145 path: tests_base
146 .join("Tests")
147 .join(format!("{module_name}E2ETests"))
148 .join(filename),
149 content,
150 generated_header: true,
151 });
152 }
153
154 Ok(files)
155 }
156
157 fn language_name(&self) -> &'static str {
158 "swift"
159 }
160}
161
162fn render_package_swift(
167 module_name: &str,
168 registry_url: &str,
169 pkg_path: &str,
170 pkg_version: &str,
171 dep_mode: crate::config::DependencyMode,
172) -> String {
173 let min_macos = toolchain::SWIFT_MIN_MACOS;
174
175 let (dep_block, product_dep) = match dep_mode {
179 crate::config::DependencyMode::Registry => {
180 let dep = format!(r#" .package(url: "{registry_url}", from: "{pkg_version}")"#);
181 let pkg_id = registry_url
182 .trim_end_matches('/')
183 .trim_end_matches(".git")
184 .split('/')
185 .next_back()
186 .unwrap_or(module_name);
187 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
188 (dep, prod)
189 }
190 crate::config::DependencyMode::Local => {
191 let pkg_id = pkg_path
196 .trim_end_matches('/')
197 .split('/')
198 .next_back()
199 .unwrap_or(module_name);
200 let dep = format!(r#" .package(path: "{pkg_path}")"#);
201 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
202 (dep, prod)
203 }
204 };
205 let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
208 format!(
211 r#"// swift-tools-version: 6.0
212import PackageDescription
213
214let package = Package(
215 name: "E2eSwift",
216 platforms: [
217 .macOS(.v{min_macos_major}),
218 .iOS(.v14),
219 ],
220 dependencies: [
221{dep_block},
222 ],
223 targets: [
224 .testTarget(
225 name: "{module_name}E2ETests",
226 dependencies: [{product_dep}]
227 ),
228 ]
229)
230"#
231 )
232}
233
234#[allow(clippy::too_many_arguments)]
235fn render_test_file(
236 category: &str,
237 fixtures: &[&Fixture],
238 e2e_config: &E2eConfig,
239 module_name: &str,
240 class_name: &str,
241 function_name: &str,
242 result_var: &str,
243 args: &[crate::config::ArgMapping],
244 field_resolver: &FieldResolver,
245 result_is_simple: bool,
246 enum_fields: &HashSet<String>,
247 client_factory: Option<&str>,
248) -> String {
249 let needs_chdir = fixtures.iter().any(|f| {
256 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
257 call_config
258 .args
259 .iter()
260 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
261 });
262
263 let mut out = String::new();
264 out.push_str(&hash::header(CommentStyle::DoubleSlash));
265 let _ = writeln!(out, "import XCTest");
266 let _ = writeln!(out, "import Foundation");
267 let _ = writeln!(out, "import {module_name}");
268 let _ = writeln!(out, "import RustBridge");
269 let _ = writeln!(out);
270 let _ = writeln!(out, "/// E2e tests for category: {category}.");
271 let _ = writeln!(out, "final class {class_name}: XCTestCase {{");
272
273 if needs_chdir {
274 let _ = writeln!(out, " override class func setUp() {{");
282 let _ = writeln!(out, " super.setUp()");
283 let _ = writeln!(out, " let _testDocs = URL(fileURLWithPath: #filePath)");
284 let _ = writeln!(out, " .deletingLastPathComponent() // <Module>Tests/");
285 let _ = writeln!(out, " .deletingLastPathComponent() // Tests/");
286 let _ = writeln!(out, " .deletingLastPathComponent() // swift/");
287 let _ = writeln!(out, " .deletingLastPathComponent() // packages/");
288 let _ = writeln!(out, " .deletingLastPathComponent() // <repo root>");
289 let _ = writeln!(
290 out,
291 " .appendingPathComponent(\"{}\")",
292 e2e_config.test_documents_dir
293 );
294 let _ = writeln!(
295 out,
296 " if FileManager.default.fileExists(atPath: _testDocs.path) {{"
297 );
298 let _ = writeln!(
299 out,
300 " FileManager.default.changeCurrentDirectoryPath(_testDocs.path)"
301 );
302 let _ = writeln!(out, " }}");
303 let _ = writeln!(out, " }}");
304 let _ = writeln!(out);
305 }
306
307 for fixture in fixtures {
308 if fixture.is_http_test() {
309 render_http_test_method(&mut out, fixture);
310 } else {
311 render_test_method(
312 &mut out,
313 fixture,
314 e2e_config,
315 function_name,
316 result_var,
317 args,
318 field_resolver,
319 result_is_simple,
320 enum_fields,
321 client_factory,
322 );
323 }
324 let _ = writeln!(out);
325 }
326
327 let _ = writeln!(out, "}}");
328 out
329}
330
331struct SwiftTestClientRenderer;
338
339impl client::TestClientRenderer for SwiftTestClientRenderer {
340 fn language_name(&self) -> &'static str {
341 "swift"
342 }
343
344 fn sanitize_test_name(&self, id: &str) -> String {
345 sanitize_ident(id).to_upper_camel_case()
347 }
348
349 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
355 let _ = writeln!(out, " /// {description}");
356 let _ = writeln!(out, " func test{fn_name}() throws {{");
357 if let Some(reason) = skip_reason {
358 let escaped = escape_swift(reason);
359 let _ = writeln!(out, " try XCTSkipIf(true, \"{escaped}\")");
360 }
361 }
362
363 fn render_test_close(&self, out: &mut String) {
364 let _ = writeln!(out, " }}");
365 }
366
367 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
374 let method = ctx.method.to_uppercase();
375 let fixture_path = escape_swift(ctx.path);
376
377 let _ = writeln!(
378 out,
379 " let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
380 );
381 let _ = writeln!(
382 out,
383 " var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
384 );
385 let _ = writeln!(out, " _req.httpMethod = \"{method}\"");
386
387 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
389 header_pairs.sort_by_key(|(k, _)| k.as_str());
390 for (k, v) in &header_pairs {
391 let expanded_v = expand_fixture_templates(v);
392 let ek = escape_swift(k);
393 let ev = escape_swift(&expanded_v);
394 let _ = writeln!(out, " _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
395 }
396
397 if let Some(body) = ctx.body {
399 let json_str = serde_json::to_string(body).unwrap_or_default();
400 let escaped_body = escape_swift(&json_str);
401 let _ = writeln!(out, " _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
402 let _ = writeln!(
403 out,
404 " _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
405 );
406 }
407
408 let _ = writeln!(out, " var {}: HTTPURLResponse?", ctx.response_var);
409 let _ = writeln!(out, " var _responseData: Data?");
410 let _ = writeln!(out, " let _sema = DispatchSemaphore(value: 0)");
411 let _ = writeln!(
412 out,
413 " URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
414 );
415 let _ = writeln!(out, " {} = resp as? HTTPURLResponse", ctx.response_var);
416 let _ = writeln!(out, " _responseData = data");
417 let _ = writeln!(out, " _sema.signal()");
418 let _ = writeln!(out, " }}.resume()");
419 let _ = writeln!(out, " _sema.wait()");
420 let _ = writeln!(out, " let _resp = try XCTUnwrap({})", ctx.response_var);
421 }
422
423 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
424 let _ = writeln!(out, " XCTAssertEqual(_resp.statusCode, {status})");
425 }
426
427 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
428 let lower_name = name.to_lowercase();
429 let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
430 match expected {
431 "<<present>>" => {
432 let _ = writeln!(out, " XCTAssertNotNil({header_expr})");
433 }
434 "<<absent>>" => {
435 let _ = writeln!(out, " XCTAssertNil({header_expr})");
436 }
437 "<<uuid>>" => {
438 let _ = writeln!(out, " let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
439 let _ = writeln!(
440 out,
441 " 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))"
442 );
443 }
444 exact => {
445 let escaped = escape_swift(exact);
446 let _ = writeln!(out, " XCTAssertEqual({header_expr}, \"{escaped}\")");
447 }
448 }
449 }
450
451 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
452 if let serde_json::Value::String(s) = expected {
453 let escaped = escape_swift(s);
454 let _ = writeln!(
455 out,
456 " let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
457 );
458 let _ = writeln!(
459 out,
460 " XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
461 );
462 } else {
463 let json_str = serde_json::to_string(expected).unwrap_or_default();
464 let escaped = escape_swift(&json_str);
465 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
466 let _ = writeln!(
467 out,
468 " let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
469 );
470 let _ = writeln!(
471 out,
472 " let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
473 );
474 let _ = writeln!(
475 out,
476 " XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
477 );
478 }
479 }
480
481 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
482 if let Some(obj) = expected.as_object() {
483 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
484 let _ = writeln!(
485 out,
486 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
487 );
488 for (key, val) in obj {
489 let escaped_key = escape_swift(key);
490 let swift_val = json_to_swift(val);
491 let _ = writeln!(
492 out,
493 " XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
494 );
495 }
496 }
497 }
498
499 fn render_assert_validation_errors(
500 &self,
501 out: &mut String,
502 _response_var: &str,
503 errors: &[ValidationErrorExpectation],
504 ) {
505 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
506 let _ = writeln!(
507 out,
508 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
509 );
510 let _ = writeln!(
511 out,
512 " let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
513 );
514 for ve in errors {
515 let escaped_msg = escape_swift(&ve.msg);
516 let _ = writeln!(
517 out,
518 " XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
519 );
520 }
521 }
522}
523
524fn render_http_test_method(out: &mut String, fixture: &Fixture) {
529 let Some(http) = &fixture.http else {
530 return;
531 };
532
533 if http.expected_response.status_code == 101 {
535 let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
536 let description = fixture.description.replace('"', "\\\"");
537 let _ = writeln!(out, " /// {description}");
538 let _ = writeln!(out, " func test{method_name}() throws {{");
539 let _ = writeln!(
540 out,
541 " try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
542 );
543 let _ = writeln!(out, " }}");
544 return;
545 }
546
547 client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
548}
549
550#[allow(clippy::too_many_arguments)]
555fn render_test_method(
556 out: &mut String,
557 fixture: &Fixture,
558 e2e_config: &E2eConfig,
559 _function_name: &str,
560 _result_var: &str,
561 _args: &[crate::config::ArgMapping],
562 field_resolver: &FieldResolver,
563 result_is_simple: bool,
564 enum_fields: &HashSet<String>,
565 global_client_factory: Option<&str>,
566) {
567 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
569 let lang = "swift";
570 let call_overrides = call_config.overrides.get(lang);
571 let function_name = call_overrides
572 .and_then(|o| o.function.as_ref())
573 .cloned()
574 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
575 let client_factory: Option<&str> = call_overrides
577 .and_then(|o| o.client_factory.as_deref())
578 .or(global_client_factory);
579 let result_var = &call_config.result_var;
580 let args = &call_config.args;
581 let result_is_bytes_any_lang =
588 call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
589 eprintln!(
590 "[swift debug] fixture={} call={:?} result_is_bytes={} any_override_bytes={} overrides={}",
591 fixture.id,
592 fixture.call,
593 call_config.result_is_bytes,
594 call_config.overrides.values().any(|o| o.result_is_bytes),
595 call_config.overrides.len()
596 );
597 let result_is_simple = call_config.result_is_simple
598 || call_overrides.is_some_and(|o| o.result_is_simple)
599 || result_is_simple
600 || result_is_bytes_any_lang;
601 let result_is_array = call_config.result_is_array;
602 let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);
607
608 let method_name = fixture.id.to_upper_camel_case();
609 let description = &fixture.description;
610 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
611 let is_async = call_config.r#async;
612
613 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
615 let collect_snippet_opt = if is_streaming && !expects_error {
616 crate::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(lang, result_var, "chunks")
617 } else {
618 None
619 };
620 if is_streaming && !expects_error && collect_snippet_opt.is_none() {
627 if is_async {
628 let _ = writeln!(out, " func test{method_name}() async throws {{");
629 } else {
630 let _ = writeln!(out, " func test{method_name}() throws {{");
631 }
632 let _ = writeln!(out, " // {description}");
633 let _ = writeln!(
634 out,
635 " try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
636 fixture.id
637 );
638 let _ = writeln!(out, " }}");
639 return;
640 }
641 let collect_snippet = collect_snippet_opt.unwrap_or_default();
642
643 let has_unresolvable_json_object_arg = {
650 let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
651 options_via.is_none() && args.iter().any(|a| a.arg_type == "json_object" && a.name != "config")
652 };
653
654 if has_unresolvable_json_object_arg {
655 if is_async {
656 let _ = writeln!(out, " func test{method_name}() async throws {{");
657 } else {
658 let _ = writeln!(out, " func test{method_name}() throws {{");
659 }
660 let _ = writeln!(out, " // {description}");
661 let _ = writeln!(
662 out,
663 " try XCTSkipIf(true, \"swift: json_object request construction requires options_via configuration (fixture: {})\");",
664 fixture.id
665 );
666 let _ = writeln!(out, " }}");
667 return;
668 }
669
670 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
674
675 let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
680 let per_call = call_overrides.map(|o| &o.enum_fields);
681 if let Some(pc) = per_call {
682 if !pc.is_empty() {
683 let mut merged = enum_fields.clone();
684 merged.extend(pc.keys().cloned());
685 std::borrow::Cow::Owned(merged)
686 } else {
687 std::borrow::Cow::Borrowed(enum_fields)
688 }
689 } else {
690 std::borrow::Cow::Borrowed(enum_fields)
691 }
692 };
693
694 let options_via_str: Option<&str> = call_overrides.and_then(|o| o.options_via.as_deref());
695 let options_type_str: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
696 let handle_config_fn_owned: Option<String> = call_config
700 .overrides
701 .get("c")
702 .and_then(|c| c.c_engine_factory.as_deref())
703 .map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
704 let (setup_lines, args_str) = build_args_and_setup(
705 &fixture.input,
706 args,
707 &fixture.id,
708 fixture.has_host_root_route(),
709 &function_name,
710 options_via_str,
711 options_type_str,
712 handle_config_fn_owned.as_deref(),
713 );
714
715 let args_str = if extra_args.is_empty() {
717 args_str
718 } else if args_str.is_empty() {
719 extra_args.join(", ")
720 } else {
721 format!("{args_str}, {}", extra_args.join(", "))
722 };
723
724 let has_mock = fixture.mock_response.is_some();
729 let (call_setup, call_expr) = if let Some(_factory) = client_factory {
730 let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
731 let mock_url = if fixture.has_host_root_route() {
732 format!(
733 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\")",
734 fixture.id
735 )
736 } else {
737 format!(
738 "ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\"",
739 fixture.id
740 )
741 };
742 let client_constructor = if has_mock {
743 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
744 } else {
745 if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
747 format!(
748 "let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n \
749 let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n \
750 let _client = try DefaultClient(apiKey: _apiKey ?? \"test-key\", baseUrl: _baseUrl)"
751 )
752 } else {
753 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
754 }
755 };
756 let expr = if is_async {
757 format!("try await _client.{function_name}({args_str})")
758 } else {
759 format!("try _client.{function_name}({args_str})")
760 };
761 (Some(client_constructor), expr)
762 } else {
763 let expr = if is_async {
765 format!("try await {function_name}({args_str})")
766 } else {
767 format!("try {function_name}({args_str})")
768 };
769 (None, expr)
770 };
771 let _ = function_name;
773
774 if is_async {
775 let _ = writeln!(out, " func test{method_name}() async throws {{");
776 } else {
777 let _ = writeln!(out, " func test{method_name}() throws {{");
778 }
779 let _ = writeln!(out, " // {description}");
780
781 if expects_error {
782 if is_async {
786 let _ = writeln!(out, " do {{");
791 for line in &setup_lines {
792 let _ = writeln!(out, " {line}");
793 }
794 if let Some(setup) = &call_setup {
795 let _ = writeln!(out, " {setup}");
796 }
797 let _ = writeln!(out, " _ = {call_expr}");
798 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
799 let _ = writeln!(out, " }} catch {{");
800 let _ = writeln!(out, " // success");
801 let _ = writeln!(out, " }}");
802 } else {
803 let _ = writeln!(out, " do {{");
810 for line in &setup_lines {
811 let _ = writeln!(out, " {line}");
812 }
813 if let Some(setup) = &call_setup {
814 let _ = writeln!(out, " {setup}");
815 }
816 let _ = writeln!(out, " _ = {call_expr}");
817 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
818 let _ = writeln!(out, " }} catch {{");
819 let _ = writeln!(out, " // success");
820 let _ = writeln!(out, " }}");
821 }
822 let _ = writeln!(out, " }}");
823 return;
824 }
825
826 for line in &setup_lines {
827 let _ = writeln!(out, " {line}");
828 }
829
830 if let Some(setup) = &call_setup {
832 let _ = writeln!(out, " {setup}");
833 }
834
835 let _ = writeln!(out, " let {result_var} = {call_expr}");
836
837 if !collect_snippet.is_empty() {
840 for line in collect_snippet.lines() {
841 let _ = writeln!(out, " {line}");
842 }
843 }
844
845 for assertion in &fixture.assertions {
846 render_assertion(
847 out,
848 assertion,
849 result_var,
850 field_resolver,
851 result_is_simple,
852 result_is_array,
853 result_is_option,
854 &effective_enum_fields,
855 );
856 }
857
858 let _ = writeln!(out, " }}");
859}
860
861#[allow(clippy::too_many_arguments)]
862fn build_args_and_setup(
876 input: &serde_json::Value,
877 args: &[crate::config::ArgMapping],
878 fixture_id: &str,
879 has_host_root_route: bool,
880 function_name: &str,
881 options_via: Option<&str>,
882 options_type: Option<&str>,
883 handle_config_fn: Option<&str>,
884) -> (Vec<String>, String) {
885 if args.is_empty() {
886 return (Vec::new(), String::new());
887 }
888
889 let mut setup_lines: Vec<String> = Vec::new();
890 let mut parts: Vec<String> = Vec::new();
891
892 let later_emits: Vec<bool> = (0..args.len())
897 .map(|i| {
898 args.iter().skip(i + 1).any(|a| {
899 let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
900 let v = input.get(f);
901 let has_value = matches!(v, Some(x) if !x.is_null());
902 has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
903 })
904 })
905 .collect();
906
907 for (idx, arg) in args.iter().enumerate() {
908 if arg.arg_type == "mock_url" {
909 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
910 let url_expr = if has_host_root_route {
911 format!(
912 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\")"
913 )
914 } else {
915 format!("ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"")
916 };
917 setup_lines.push(format!("let {} = {url_expr}", arg.name));
918 parts.push(arg.name.clone());
919 continue;
920 }
921
922 if arg.arg_type == "handle" {
923 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
924 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
925 let config_val = input.get(field);
926 let has_config = config_val
927 .is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
928 if has_config {
929 if let Some(from_json_fn) = handle_config_fn {
930 let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
931 let escaped = escape_swift_str(&json_str);
932 let config_var = format!("{}Config", arg.name.to_lower_camel_case());
933 setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
934 setup_lines.push(format!("let {var_name} = try createEngine({config_var})"));
935 } else {
936 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
937 }
938 } else {
939 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
940 }
941 parts.push(var_name);
942 continue;
943 }
944
945 if arg.arg_type == "bytes" {
950 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
951 let val = input.get(field);
952 match val {
953 None | Some(serde_json::Value::Null) if arg.optional => {
954 if later_emits[idx] {
955 parts.push("nil".to_string());
956 }
957 }
958 None | Some(serde_json::Value::Null) => {
959 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
960 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
961 parts.push(var_name);
962 }
963 Some(serde_json::Value::String(s)) => {
964 let escaped = escape_swift(s);
965 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
966 let data_var = format!("{}Data", arg.name.to_lower_camel_case());
967 setup_lines.push(format!(
968 "let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
969 ));
970 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
971 setup_lines.push(format!("for _byte in {data_var} {{ {var_name}.push(value: _byte) }}"));
972 parts.push(var_name);
973 }
974 Some(serde_json::Value::Array(arr)) => {
975 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
976 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
977 for v in arr {
978 if let Some(n) = v.as_u64() {
979 setup_lines.push(format!("{var_name}.push(value: UInt8({n}))"));
980 }
981 }
982 parts.push(var_name);
983 }
984 Some(other) => {
985 let json_str = serde_json::to_string(other).unwrap_or_default();
987 let escaped = escape_swift(&json_str);
988 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
989 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
990 setup_lines.push(format!(
991 "for _byte in Array(\"{escaped}\".utf8) {{ {var_name}.push(value: _byte) }}"
992 ));
993 parts.push(var_name);
994 }
995 }
996 continue;
997 }
998
999 let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
1004 let is_batch_fn = function_name.starts_with("batch") || function_name.starts_with("Batch");
1005 if is_config_arg && !is_batch_fn {
1006 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1007 let val = input.get(field);
1008 let json_str = match val {
1009 None | Some(serde_json::Value::Null) => "{}".to_string(),
1010 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1011 };
1012 let escaped = escape_swift(&json_str);
1013 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1014 setup_lines.push(format!("let {var_name} = try extractionConfigFromJson(\"{escaped}\")"));
1015 parts.push(var_name);
1016 continue;
1017 }
1018
1019 if arg.arg_type == "json_object" && options_via == Some("from_json") {
1024 if let Some(type_name) = options_type {
1025 let resolved_val = super::resolve_field(input, &arg.field);
1026 let json_str = match resolved_val {
1027 serde_json::Value::Null => "{}".to_string(),
1028 v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1029 };
1030 let escaped = escape_swift(&json_str);
1031 let var_name = format!("_{}", arg.name.to_lower_camel_case());
1032 let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
1033 setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1034 parts.push(var_name);
1035 continue;
1036 }
1037 }
1038
1039 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1040 let val = input.get(field);
1041 match val {
1042 None | Some(serde_json::Value::Null) if arg.optional => {
1043 if later_emits[idx] {
1047 parts.push("nil".to_string());
1048 }
1049 }
1050 None | Some(serde_json::Value::Null) => {
1051 let default_val = match arg.arg_type.as_str() {
1052 "string" => "\"\"".to_string(),
1053 "int" | "integer" => "0".to_string(),
1054 "float" | "number" => "0.0".to_string(),
1055 "bool" | "boolean" => "false".to_string(),
1056 _ => "nil".to_string(),
1057 };
1058 parts.push(default_val);
1059 }
1060 Some(v) => {
1061 parts.push(json_to_swift(v));
1062 }
1063 }
1064 }
1065
1066 (setup_lines, parts.join(", "))
1067}
1068
1069#[allow(clippy::too_many_arguments)]
1070fn render_assertion(
1071 out: &mut String,
1072 assertion: &Assertion,
1073 result_var: &str,
1074 field_resolver: &FieldResolver,
1075 result_is_simple: bool,
1076 result_is_array: bool,
1077 result_is_option: bool,
1078 enum_fields: &HashSet<String>,
1079) {
1080 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1085 if let Some(f) = &assertion.field {
1088 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1089 if let Some(expr) =
1090 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "swift", "chunks")
1091 {
1092 let line = match assertion.assertion_type.as_str() {
1093 "count_min" => {
1094 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1095 format!(" XCTAssertGreaterThanOrEqual(chunks.count, {n})\n")
1096 } else {
1097 String::new()
1098 }
1099 }
1100 "count_equals" => {
1101 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1102 format!(" XCTAssertEqual(chunks.count, {n})\n")
1103 } else {
1104 String::new()
1105 }
1106 }
1107 "equals" => {
1108 if let Some(serde_json::Value::String(s)) = &assertion.value {
1109 let escaped = escape_swift(s);
1110 format!(" XCTAssertEqual({expr}, \"{escaped}\")\n")
1111 } else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
1112 format!(" XCTAssertEqual({expr}, {b})\n")
1113 } else {
1114 String::new()
1115 }
1116 }
1117 "not_empty" => {
1118 format!(" XCTAssertFalse({expr}.isEmpty, \"expected non-empty\")\n")
1119 }
1120 "is_empty" => {
1121 format!(" XCTAssertTrue({expr}.isEmpty, \"expected empty\")\n")
1122 }
1123 "is_true" => {
1124 format!(" XCTAssertTrue({expr})\n")
1125 }
1126 "is_false" => {
1127 format!(" XCTAssertFalse({expr})\n")
1128 }
1129 "greater_than" => {
1130 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1131 format!(" XCTAssertGreaterThan(chunks.count, {n})\n")
1132 } else {
1133 String::new()
1134 }
1135 }
1136 "contains" => {
1137 if let Some(serde_json::Value::String(s)) = &assertion.value {
1138 let escaped = escape_swift(s);
1139 format!(
1140 " XCTAssertTrue({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\")\n"
1141 )
1142 } else {
1143 String::new()
1144 }
1145 }
1146 _ => format!(
1147 " // streaming field '{f}': assertion type '{}' not rendered\n",
1148 assertion.assertion_type
1149 ),
1150 };
1151 if !line.is_empty() {
1152 out.push_str(&line);
1153 }
1154 }
1155 return;
1156 }
1157 }
1158
1159 if let Some(f) = &assertion.field {
1161 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1162 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1163 return;
1164 }
1165 }
1166
1167 if let Some(f) = &assertion.field {
1172 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1173 let _ = writeln!(
1174 out,
1175 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Swift)"
1176 );
1177 return;
1178 }
1179 }
1180
1181 let field_is_enum = assertion
1183 .field
1184 .as_deref()
1185 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1186
1187 let field_is_optional = assertion.field.as_deref().is_some_and(|f| {
1188 !f.is_empty() && (field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f)))
1189 });
1190 let field_is_array = assertion.field.as_deref().is_some_and(|f| {
1191 !f.is_empty()
1192 && (field_resolver.is_array(f)
1193 || field_resolver.is_array(field_resolver.resolve(f))
1194 || field_resolver.is_collection_root(f)
1195 || field_resolver.is_collection_root(field_resolver.resolve(f)))
1196 });
1197
1198 let field_expr_raw = if result_is_simple {
1199 result_var.to_string()
1200 } else {
1201 match &assertion.field {
1202 Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
1203 _ => result_var.to_string(),
1204 }
1205 };
1206
1207 let local_suffix = {
1217 use std::hash::{Hash, Hasher};
1218 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1219 assertion.field.hash(&mut hasher);
1220 assertion
1221 .value
1222 .as_ref()
1223 .map(|v| v.to_string())
1224 .unwrap_or_default()
1225 .hash(&mut hasher);
1226 format!(
1227 "{}_{:x}",
1228 assertion.assertion_type.replace(['-', '.'], "_"),
1229 hasher.finish() & 0xffff_ffff,
1230 )
1231 };
1232 let (vec_setup, field_expr) = materialise_vec_temporaries(&field_expr_raw, &local_suffix);
1233 let field_uses_traversal = assertion.field.as_deref().is_some_and(|f| f.contains("[]."));
1238 let traversal_skips_field_expr = field_uses_traversal
1239 && matches!(
1240 assertion.assertion_type.as_str(),
1241 "contains" | "not_contains" | "not_empty" | "is_empty"
1242 );
1243 if !traversal_skips_field_expr {
1244 for line in &vec_setup {
1245 let _ = writeln!(out, " {line}");
1246 }
1247 }
1248
1249 let accessor_is_optional = field_expr.contains("?.");
1255
1256 let string_expr = if field_is_enum && (field_is_optional || accessor_is_optional) {
1265 format!("({field_expr}?.toString() ?? \"\")")
1268 } else if field_is_enum {
1269 format!("{field_expr}.toString()")
1274 } else if field_is_optional {
1275 format!("({field_expr}?.toString() ?? \"\")")
1277 } else if accessor_is_optional {
1278 format!("({field_expr}.toString() ?? \"\")")
1281 } else {
1282 format!("{field_expr}.toString()")
1283 };
1284
1285 match assertion.assertion_type.as_str() {
1286 "equals" => {
1287 if let Some(expected) = &assertion.value {
1288 let swift_val = json_to_swift(expected);
1289 if expected.is_string() {
1290 if field_is_enum {
1291 let trim_expr = format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespaces)");
1295 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1296 } else {
1297 let trim_expr = format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespaces)");
1302 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1303 }
1304 } else {
1305 let _ = writeln!(out, " XCTAssertEqual({field_expr}, {swift_val})");
1306 }
1307 }
1308 }
1309 "contains" => {
1310 if let Some(expected) = &assertion.value {
1311 let swift_val = json_to_swift(expected);
1312 let no_field = assertion.field.as_deref().is_none_or(|f| f.is_empty());
1315 if result_is_simple && result_is_array && no_field {
1316 let _ = writeln!(
1319 out,
1320 " XCTAssertTrue({result_var}.map {{ $0.as_str().toString() }}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1321 );
1322 } else {
1323 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1325 if let Some(dot) = f.find("[].") {
1326 let array_part = &f[..dot];
1327 let elem_part = &f[dot + 3..];
1328 let line = swift_traversal_contains_assert(
1329 array_part,
1330 elem_part,
1331 f,
1332 &swift_val,
1333 result_var,
1334 false,
1335 &format!("expected to contain: \\({swift_val})"),
1336 enum_fields,
1337 field_resolver,
1338 );
1339 let _ = writeln!(out, "{line}");
1340 true
1341 } else {
1342 false
1343 }
1344 } else {
1345 false
1346 };
1347 if !traversal_handled {
1348 let field_is_array = assertion
1350 .field
1351 .as_deref()
1352 .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
1353 if field_is_array {
1354 let contains_expr =
1355 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1356 let _ = writeln!(
1357 out,
1358 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1359 );
1360 } else if field_is_enum {
1361 let _ = writeln!(
1364 out,
1365 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1366 );
1367 } else {
1368 let _ = writeln!(
1369 out,
1370 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1371 );
1372 }
1373 }
1374 }
1375 }
1376 }
1377 "contains_all" => {
1378 if let Some(values) = &assertion.values {
1379 if let Some(f) = assertion.field.as_deref() {
1381 if let Some(dot) = f.find("[].") {
1382 let array_part = &f[..dot];
1383 let elem_part = &f[dot + 3..];
1384 for val in values {
1385 let swift_val = json_to_swift(val);
1386 let line = swift_traversal_contains_assert(
1387 array_part,
1388 elem_part,
1389 f,
1390 &swift_val,
1391 result_var,
1392 false,
1393 &format!("expected to contain: \\({swift_val})"),
1394 enum_fields,
1395 field_resolver,
1396 );
1397 let _ = writeln!(out, "{line}");
1398 }
1399 } else {
1401 let field_is_array = field_resolver.is_array(field_resolver.resolve(f));
1403 if field_is_array {
1404 let contains_expr =
1405 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1406 for val in values {
1407 let swift_val = json_to_swift(val);
1408 let _ = writeln!(
1409 out,
1410 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1411 );
1412 }
1413 } else if field_is_enum {
1414 for val in values {
1417 let swift_val = json_to_swift(val);
1418 let _ = writeln!(
1419 out,
1420 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1421 );
1422 }
1423 } else {
1424 for val in values {
1425 let swift_val = json_to_swift(val);
1426 let _ = writeln!(
1427 out,
1428 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1429 );
1430 }
1431 }
1432 }
1433 } else {
1434 for val in values {
1436 let swift_val = json_to_swift(val);
1437 let _ = writeln!(
1438 out,
1439 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1440 );
1441 }
1442 }
1443 }
1444 }
1445 "not_contains" => {
1446 if let Some(expected) = &assertion.value {
1447 let swift_val = json_to_swift(expected);
1448 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1450 if let Some(dot) = f.find("[].") {
1451 let array_part = &f[..dot];
1452 let elem_part = &f[dot + 3..];
1453 let line = swift_traversal_contains_assert(
1454 array_part,
1455 elem_part,
1456 f,
1457 &swift_val,
1458 result_var,
1459 true,
1460 &format!("expected NOT to contain: \\({swift_val})"),
1461 enum_fields,
1462 field_resolver,
1463 );
1464 let _ = writeln!(out, "{line}");
1465 true
1466 } else {
1467 false
1468 }
1469 } else {
1470 false
1471 };
1472 if !traversal_handled {
1473 let _ = writeln!(
1474 out,
1475 " XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
1476 );
1477 }
1478 }
1479 }
1480 "not_empty" => {
1481 let traversal_not_empty_handled = if let Some(f) = assertion.field.as_deref() {
1488 if let Some(dot) = f.find("[].") {
1489 let array_part = &f[..dot];
1490 let elem_part = &f[dot + 3..];
1491 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1492 let resolved_full = field_resolver.resolve(f);
1493 let resolved_elem_part = resolved_full
1494 .find("[].")
1495 .map(|d| &resolved_full[d + 3..])
1496 .unwrap_or(elem_part);
1497 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1498 let elem_is_enum = enum_fields.contains(f) || enum_fields.contains(resolved_full);
1499 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1500 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1501 let elem_str = if elem_is_enum {
1502 format!("{elem_accessor}.to_string().toString()")
1503 } else if elem_is_optional {
1504 format!("({elem_accessor}?.toString() ?? \"\")")
1505 } else {
1506 format!("{elem_accessor}.toString()")
1507 };
1508 let _ = writeln!(
1509 out,
1510 " XCTAssertTrue({array_accessor}.contains(where: {{ !{elem_str}.isEmpty }}), \"expected non-empty value\")"
1511 );
1512 true
1513 } else {
1514 false
1515 }
1516 } else {
1517 false
1518 };
1519 if !traversal_not_empty_handled {
1520 if bare_result_is_option {
1521 let _ = writeln!(out, " XCTAssertNotNil({result_var}, \"expected non-nil value\")");
1522 } else if field_is_optional {
1523 let _ = writeln!(out, " XCTAssertNotNil({field_expr}, \"expected non-nil value\")");
1524 } else if field_is_array {
1525 let _ = writeln!(
1526 out,
1527 " XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
1528 );
1529 } else if result_is_simple {
1530 let _ = writeln!(
1532 out,
1533 " XCTAssertFalse({result_var}.isEmpty, \"expected non-empty value\")"
1534 );
1535 } else {
1536 let len_expr = if accessor_is_optional {
1545 format!("({field_expr}.len() ?? 0)")
1546 } else {
1547 format!("{field_expr}.len()")
1548 };
1549 let _ = writeln!(
1550 out,
1551 " XCTAssertGreaterThan({len_expr}, 0, \"expected non-empty value\")"
1552 );
1553 }
1554 }
1555 }
1556 "is_empty" => {
1557 if bare_result_is_option {
1558 let _ = writeln!(out, " XCTAssertNil({result_var}, \"expected nil value\")");
1559 } else if field_is_optional {
1560 let _ = writeln!(out, " XCTAssertNil({field_expr}, \"expected nil value\")");
1561 } else if field_is_array {
1562 let _ = writeln!(
1563 out,
1564 " XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
1565 );
1566 } else {
1567 let len_expr = if accessor_is_optional {
1571 format!("({field_expr}.len() ?? 0)")
1572 } else {
1573 format!("{field_expr}.len()")
1574 };
1575 let _ = writeln!(out, " XCTAssertEqual({len_expr}, 0, \"expected empty value\")");
1576 }
1577 }
1578 "contains_any" => {
1579 if let Some(values) = &assertion.values {
1580 let checks: Vec<String> = values
1581 .iter()
1582 .map(|v| {
1583 let swift_val = json_to_swift(v);
1584 format!("{string_expr}.contains({swift_val})")
1585 })
1586 .collect();
1587 let joined = checks.join(" || ");
1588 let _ = writeln!(
1589 out,
1590 " XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
1591 );
1592 }
1593 }
1594 "greater_than" => {
1595 if let Some(val) = &assertion.value {
1596 let swift_val = json_to_swift(val);
1597 let field_is_optional = accessor_is_optional
1600 || assertion.field.as_deref().is_some_and(|f| {
1601 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1602 });
1603 let compare_expr = if field_is_optional {
1604 format!("({field_expr} ?? 0)")
1605 } else {
1606 field_expr.clone()
1607 };
1608 let _ = writeln!(out, " XCTAssertGreaterThan({compare_expr}, {swift_val})");
1609 }
1610 }
1611 "less_than" => {
1612 if let Some(val) = &assertion.value {
1613 let swift_val = json_to_swift(val);
1614 let field_is_optional = accessor_is_optional
1615 || assertion.field.as_deref().is_some_and(|f| {
1616 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1617 });
1618 let compare_expr = if field_is_optional {
1619 format!("({field_expr} ?? 0)")
1620 } else {
1621 field_expr.clone()
1622 };
1623 let _ = writeln!(out, " XCTAssertLessThan({compare_expr}, {swift_val})");
1624 }
1625 }
1626 "greater_than_or_equal" => {
1627 if let Some(val) = &assertion.value {
1628 let swift_val = json_to_swift(val);
1629 let field_is_optional = accessor_is_optional
1632 || assertion.field.as_deref().is_some_and(|f| {
1633 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1634 });
1635 let compare_expr = if field_is_optional {
1636 format!("({field_expr} ?? 0)")
1637 } else {
1638 field_expr.clone()
1639 };
1640 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({compare_expr}, {swift_val})");
1641 }
1642 }
1643 "less_than_or_equal" => {
1644 if let Some(val) = &assertion.value {
1645 let swift_val = json_to_swift(val);
1646 let field_is_optional = accessor_is_optional
1647 || assertion.field.as_deref().is_some_and(|f| {
1648 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1649 });
1650 let compare_expr = if field_is_optional {
1651 format!("({field_expr} ?? 0)")
1652 } else {
1653 field_expr.clone()
1654 };
1655 let _ = writeln!(out, " XCTAssertLessThanOrEqual({compare_expr}, {swift_val})");
1656 }
1657 }
1658 "starts_with" => {
1659 if let Some(expected) = &assertion.value {
1660 let swift_val = json_to_swift(expected);
1661 let _ = writeln!(
1662 out,
1663 " XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
1664 );
1665 }
1666 }
1667 "ends_with" => {
1668 if let Some(expected) = &assertion.value {
1669 let swift_val = json_to_swift(expected);
1670 let _ = writeln!(
1671 out,
1672 " XCTAssertTrue({string_expr}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
1673 );
1674 }
1675 }
1676 "min_length" => {
1677 if let Some(val) = &assertion.value {
1678 if let Some(n) = val.as_u64() {
1679 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({string_expr}.count, {n})");
1682 }
1683 }
1684 }
1685 "max_length" => {
1686 if let Some(val) = &assertion.value {
1687 if let Some(n) = val.as_u64() {
1688 let _ = writeln!(out, " XCTAssertLessThanOrEqual({string_expr}.count, {n})");
1689 }
1690 }
1691 }
1692 "count_min" => {
1693 if let Some(val) = &assertion.value {
1694 if let Some(n) = val.as_u64() {
1695 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1699 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({count_expr}, {n})");
1700 }
1701 }
1702 }
1703 "count_equals" => {
1704 if let Some(val) = &assertion.value {
1705 if let Some(n) = val.as_u64() {
1706 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1707 let _ = writeln!(out, " XCTAssertEqual({count_expr}, {n})");
1708 }
1709 }
1710 }
1711 "is_true" => {
1712 let _ = writeln!(out, " XCTAssertTrue({field_expr})");
1713 }
1714 "is_false" => {
1715 let _ = writeln!(out, " XCTAssertFalse({field_expr})");
1716 }
1717 "matches_regex" => {
1718 if let Some(expected) = &assertion.value {
1719 let swift_val = json_to_swift(expected);
1720 let _ = writeln!(
1721 out,
1722 " XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
1723 );
1724 }
1725 }
1726 "not_error" => {
1727 }
1729 "error" => {
1730 }
1732 "method_result" => {
1733 let _ = writeln!(out, " // method_result assertions not yet implemented for Swift");
1734 }
1735 other => {
1736 panic!("Swift e2e generator: unsupported assertion type: {other}");
1737 }
1738 }
1739}
1740
1741fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String) {
1762 let Some(idx) = expr.find("()[") else {
1763 return (Vec::new(), expr.to_string());
1764 };
1765 let after_open = idx + 3; let Some(close_rel) = expr[after_open..].find(']') else {
1767 return (Vec::new(), expr.to_string());
1768 };
1769 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);
1774 let method = &expr[method_dot + 1..idx];
1775 let local = format!("_vec_{}_{}", method, name_suffix);
1776 let setup = format!("let {local} = {prefix}");
1777 let rewritten = format!("{local}{subscript}{tail}");
1778 (vec![setup], rewritten)
1779}
1780
1781fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
1784 let resolved = field_resolver.resolve(field);
1785 let parts: Vec<&str> = resolved.split('.').collect();
1786
1787 let mut out = result_var.to_string();
1790 let mut has_optional = false;
1791 let mut path_so_far = String::new();
1792 let total = parts.len();
1793 for (i, part) in parts.iter().enumerate() {
1794 let is_leaf = i == total - 1;
1795 let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
1799 (&part[..bracket_pos], Some(&part[bracket_pos..]))
1800 } else {
1801 (part, None)
1802 };
1803
1804 if !path_so_far.is_empty() {
1805 path_so_far.push('.');
1806 }
1807 let base_path = {
1811 let mut p = path_so_far.clone();
1812 p.push_str(field_name);
1813 p
1814 };
1815 path_so_far.push_str(part);
1818
1819 out.push('.');
1820 out.push_str(field_name);
1821 if let Some(sub) = subscript {
1822 let field_is_optional = field_resolver.is_optional(&base_path);
1826 if field_is_optional {
1827 out.push_str("()?");
1828 has_optional = true;
1829 } else {
1830 out.push_str("()");
1831 }
1832 out.push_str(sub);
1833 } else {
1843 out.push_str("()");
1844 if !is_leaf && field_resolver.is_optional(&base_path) {
1847 out.push('?');
1848 has_optional = true;
1849 }
1850 }
1851 }
1852 (out, has_optional)
1853}
1854
1855#[allow(clippy::too_many_arguments)]
1877fn swift_traversal_contains_assert(
1878 array_part: &str,
1879 element_part: &str,
1880 full_field: &str,
1881 val_expr: &str,
1882 result_var: &str,
1883 negate: bool,
1884 msg: &str,
1885 enum_fields: &std::collections::HashSet<String>,
1886 field_resolver: &FieldResolver,
1887) -> String {
1888 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1889 let resolved_full = field_resolver.resolve(full_field);
1890 let resolved_elem_part = resolved_full
1891 .find("[].")
1892 .map(|d| &resolved_full[d + 3..])
1893 .unwrap_or(element_part);
1894 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1895 let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
1896 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1897 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1898 let elem_str = if elem_is_enum {
1899 format!("{elem_accessor}.toString()")
1902 } else if elem_is_optional {
1903 format!("({elem_accessor}?.toString() ?? \"\")")
1904 } else {
1905 format!("{elem_accessor}.toString()")
1906 };
1907 let assert_fn = if negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
1908 format!(" {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
1909}
1910
1911fn swift_array_contains_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
1912 let Some(f) = field else {
1913 return format!("{result_var}.map {{ $0.as_str().toString() }}");
1914 };
1915 let (accessor, _has_optional) = swift_build_accessor(f, result_var, field_resolver);
1916 format!("{accessor}?.map {{ $0.as_str().toString() }}")
1919}
1920
1921fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
1930 let Some(f) = field else {
1931 return format!("{result_var}.count");
1932 };
1933 let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
1934 if field_resolver.is_optional(f) {
1936 has_optional = true;
1937 }
1938 if has_optional {
1939 if accessor.contains("?.") {
1942 format!("{accessor}.count ?? 0")
1943 } else {
1944 format!("({accessor}?.count ?? 0)")
1947 }
1948 } else {
1949 format!("{accessor}.count")
1950 }
1951}
1952
1953fn json_to_swift(value: &serde_json::Value) -> String {
1955 match value {
1956 serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
1957 serde_json::Value::Bool(b) => b.to_string(),
1958 serde_json::Value::Number(n) => n.to_string(),
1959 serde_json::Value::Null => "nil".to_string(),
1960 serde_json::Value::Array(arr) => {
1961 let items: Vec<String> = arr.iter().map(json_to_swift).collect();
1962 format!("[{}]", items.join(", "))
1963 }
1964 serde_json::Value::Object(_) => {
1965 let json_str = serde_json::to_string(value).unwrap_or_default();
1966 format!("\"{}\"", escape_swift(&json_str))
1967 }
1968 }
1969}
1970
1971fn escape_swift(s: &str) -> String {
1973 escape_swift_str(s)
1974}
1975
1976#[cfg(test)]
1977mod tests {
1978 use super::*;
1979 use crate::field_access::FieldResolver;
1980 use std::collections::{HashMap, HashSet};
1981
1982 fn make_resolver_tool_calls() -> FieldResolver {
1983 let mut optional = HashSet::new();
1987 optional.insert("choices.message.tool_calls".to_string());
1988 let mut arrays = HashSet::new();
1989 arrays.insert("choices".to_string());
1990 FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
1991 }
1992
1993 #[test]
2000 fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
2001 let resolver = make_resolver_tool_calls();
2002 let (accessor, has_optional) =
2005 swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
2006 assert!(
2009 accessor.contains("tool_calls()?[0]"),
2010 "expected `tool_calls()?[0]` for optional tool_calls, got: {accessor}"
2011 );
2012 assert!(
2014 !accessor.contains("?[0]?"),
2015 "must not emit trailing `?` after subscript index: {accessor}"
2016 );
2017 assert!(has_optional, "expected has_optional=true for optional field chain");
2019 assert!(
2021 accessor.contains("[0].function()"),
2022 "expected `.function()` (non-optional) after subscript: {accessor}"
2023 );
2024 }
2025}