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.join("Tests").join("KreuzbergE2ETests").join(filename),
146 content,
147 generated_header: true,
148 });
149 }
150
151 Ok(files)
152 }
153
154 fn language_name(&self) -> &'static str {
155 "swift"
156 }
157}
158
159fn render_package_swift(
164 module_name: &str,
165 registry_url: &str,
166 pkg_path: &str,
167 pkg_version: &str,
168 dep_mode: crate::config::DependencyMode,
169) -> String {
170 let min_macos = toolchain::SWIFT_MIN_MACOS;
171
172 let (dep_block, product_dep) = match dep_mode {
176 crate::config::DependencyMode::Registry => {
177 let dep = format!(r#" .package(url: "{registry_url}", from: "{pkg_version}")"#);
178 let pkg_id = registry_url
179 .trim_end_matches('/')
180 .trim_end_matches(".git")
181 .split('/')
182 .next_back()
183 .unwrap_or(module_name);
184 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
185 (dep, prod)
186 }
187 crate::config::DependencyMode::Local => {
188 let dep = format!(r#" .package(name: "{module_name}", path: "{pkg_path}")"#);
192 let prod = format!(r#".product(name: "{module_name}", package: "{module_name}")"#);
193 (dep, prod)
194 }
195 };
196 let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
199 format!(
202 r#"// swift-tools-version: 6.0
203import PackageDescription
204
205let package = Package(
206 name: "E2eSwift",
207 platforms: [
208 .macOS(.v{min_macos_major}),
209 .iOS(.v14),
210 ],
211 dependencies: [
212{dep_block},
213 ],
214 targets: [
215 .testTarget(
216 name: "KreuzbergE2ETests",
217 dependencies: [{product_dep}]
218 ),
219 ]
220)
221"#
222 )
223}
224
225#[allow(clippy::too_many_arguments)]
226fn render_test_file(
227 category: &str,
228 fixtures: &[&Fixture],
229 e2e_config: &E2eConfig,
230 module_name: &str,
231 class_name: &str,
232 function_name: &str,
233 result_var: &str,
234 args: &[crate::config::ArgMapping],
235 field_resolver: &FieldResolver,
236 result_is_simple: bool,
237 enum_fields: &HashSet<String>,
238 client_factory: Option<&str>,
239) -> String {
240 let needs_chdir = fixtures.iter().any(|f| {
247 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
248 call_config
249 .args
250 .iter()
251 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
252 });
253
254 let mut out = String::new();
255 out.push_str(&hash::header(CommentStyle::DoubleSlash));
256 let _ = writeln!(out, "import XCTest");
257 let _ = writeln!(out, "import Foundation");
258 let _ = writeln!(out, "import {module_name}");
259 let _ = writeln!(out, "import RustBridge");
260 let _ = writeln!(out);
261 let _ = writeln!(out, "/// E2e tests for category: {category}.");
262 let _ = writeln!(out, "final class {class_name}: XCTestCase {{");
263
264 if needs_chdir {
265 let _ = writeln!(out, " override class func setUp() {{");
273 let _ = writeln!(out, " super.setUp()");
274 let _ = writeln!(out, " let _testDocs = URL(fileURLWithPath: #filePath)");
275 let _ = writeln!(out, " .deletingLastPathComponent() // <Module>Tests/");
276 let _ = writeln!(out, " .deletingLastPathComponent() // Tests/");
277 let _ = writeln!(out, " .deletingLastPathComponent() // swift/");
278 let _ = writeln!(out, " .deletingLastPathComponent() // packages/");
279 let _ = writeln!(out, " .deletingLastPathComponent() // <repo root>");
280 let _ = writeln!(
281 out,
282 " .appendingPathComponent(\"{}\")",
283 e2e_config.test_documents_dir
284 );
285 let _ = writeln!(
286 out,
287 " if FileManager.default.fileExists(atPath: _testDocs.path) {{"
288 );
289 let _ = writeln!(
290 out,
291 " FileManager.default.changeCurrentDirectoryPath(_testDocs.path)"
292 );
293 let _ = writeln!(out, " }}");
294 let _ = writeln!(out, " }}");
295 let _ = writeln!(out);
296 }
297
298 for fixture in fixtures {
299 if fixture.is_http_test() {
300 render_http_test_method(&mut out, fixture);
301 } else {
302 render_test_method(
303 &mut out,
304 fixture,
305 e2e_config,
306 function_name,
307 result_var,
308 args,
309 field_resolver,
310 result_is_simple,
311 enum_fields,
312 client_factory,
313 );
314 }
315 let _ = writeln!(out);
316 }
317
318 let _ = writeln!(out, "}}");
319 out
320}
321
322struct SwiftTestClientRenderer;
329
330impl client::TestClientRenderer for SwiftTestClientRenderer {
331 fn language_name(&self) -> &'static str {
332 "swift"
333 }
334
335 fn sanitize_test_name(&self, id: &str) -> String {
336 sanitize_ident(id).to_upper_camel_case()
338 }
339
340 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
346 let _ = writeln!(out, " /// {description}");
347 let _ = writeln!(out, " func test{fn_name}() throws {{");
348 if let Some(reason) = skip_reason {
349 let escaped = escape_swift(reason);
350 let _ = writeln!(out, " try XCTSkipIf(true, \"{escaped}\")");
351 }
352 }
353
354 fn render_test_close(&self, out: &mut String) {
355 let _ = writeln!(out, " }}");
356 }
357
358 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
365 let method = ctx.method.to_uppercase();
366 let fixture_path = escape_swift(ctx.path);
367
368 let _ = writeln!(
369 out,
370 " let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
371 );
372 let _ = writeln!(
373 out,
374 " var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
375 );
376 let _ = writeln!(out, " _req.httpMethod = \"{method}\"");
377
378 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
380 header_pairs.sort_by_key(|(k, _)| k.as_str());
381 for (k, v) in &header_pairs {
382 let expanded_v = expand_fixture_templates(v);
383 let ek = escape_swift(k);
384 let ev = escape_swift(&expanded_v);
385 let _ = writeln!(out, " _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
386 }
387
388 if let Some(body) = ctx.body {
390 let json_str = serde_json::to_string(body).unwrap_or_default();
391 let escaped_body = escape_swift(&json_str);
392 let _ = writeln!(out, " _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
393 let _ = writeln!(
394 out,
395 " _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
396 );
397 }
398
399 let _ = writeln!(out, " var {}: HTTPURLResponse?", ctx.response_var);
400 let _ = writeln!(out, " var _responseData: Data?");
401 let _ = writeln!(out, " let _sema = DispatchSemaphore(value: 0)");
402 let _ = writeln!(
403 out,
404 " URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
405 );
406 let _ = writeln!(out, " {} = resp as? HTTPURLResponse", ctx.response_var);
407 let _ = writeln!(out, " _responseData = data");
408 let _ = writeln!(out, " _sema.signal()");
409 let _ = writeln!(out, " }}.resume()");
410 let _ = writeln!(out, " _sema.wait()");
411 let _ = writeln!(out, " let _resp = try XCTUnwrap({})", ctx.response_var);
412 }
413
414 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
415 let _ = writeln!(out, " XCTAssertEqual(_resp.statusCode, {status})");
416 }
417
418 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
419 let lower_name = name.to_lowercase();
420 let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
421 match expected {
422 "<<present>>" => {
423 let _ = writeln!(out, " XCTAssertNotNil({header_expr})");
424 }
425 "<<absent>>" => {
426 let _ = writeln!(out, " XCTAssertNil({header_expr})");
427 }
428 "<<uuid>>" => {
429 let _ = writeln!(out, " let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
430 let _ = writeln!(
431 out,
432 " 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))"
433 );
434 }
435 exact => {
436 let escaped = escape_swift(exact);
437 let _ = writeln!(out, " XCTAssertEqual({header_expr}, \"{escaped}\")");
438 }
439 }
440 }
441
442 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
443 if let serde_json::Value::String(s) = expected {
444 let escaped = escape_swift(s);
445 let _ = writeln!(
446 out,
447 " let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
448 );
449 let _ = writeln!(
450 out,
451 " XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
452 );
453 } else {
454 let json_str = serde_json::to_string(expected).unwrap_or_default();
455 let escaped = escape_swift(&json_str);
456 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
457 let _ = writeln!(
458 out,
459 " let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
460 );
461 let _ = writeln!(
462 out,
463 " let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
464 );
465 let _ = writeln!(
466 out,
467 " XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
468 );
469 }
470 }
471
472 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
473 if let Some(obj) = expected.as_object() {
474 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
475 let _ = writeln!(
476 out,
477 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
478 );
479 for (key, val) in obj {
480 let escaped_key = escape_swift(key);
481 let swift_val = json_to_swift(val);
482 let _ = writeln!(
483 out,
484 " XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
485 );
486 }
487 }
488 }
489
490 fn render_assert_validation_errors(
491 &self,
492 out: &mut String,
493 _response_var: &str,
494 errors: &[ValidationErrorExpectation],
495 ) {
496 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
497 let _ = writeln!(
498 out,
499 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
500 );
501 let _ = writeln!(
502 out,
503 " let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
504 );
505 for ve in errors {
506 let escaped_msg = escape_swift(&ve.msg);
507 let _ = writeln!(
508 out,
509 " XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
510 );
511 }
512 }
513}
514
515fn render_http_test_method(out: &mut String, fixture: &Fixture) {
520 let Some(http) = &fixture.http else {
521 return;
522 };
523
524 if http.expected_response.status_code == 101 {
526 let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
527 let description = fixture.description.replace('"', "\\\"");
528 let _ = writeln!(out, " /// {description}");
529 let _ = writeln!(out, " func test{method_name}() throws {{");
530 let _ = writeln!(
531 out,
532 " try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
533 );
534 let _ = writeln!(out, " }}");
535 return;
536 }
537
538 client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
539}
540
541#[allow(clippy::too_many_arguments)]
546fn render_test_method(
547 out: &mut String,
548 fixture: &Fixture,
549 e2e_config: &E2eConfig,
550 _function_name: &str,
551 _result_var: &str,
552 _args: &[crate::config::ArgMapping],
553 field_resolver: &FieldResolver,
554 result_is_simple: bool,
555 enum_fields: &HashSet<String>,
556 global_client_factory: Option<&str>,
557) {
558 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
560 let lang = "swift";
561 let call_overrides = call_config.overrides.get(lang);
562 let function_name = call_overrides
563 .and_then(|o| o.function.as_ref())
564 .cloned()
565 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
566 let client_factory: Option<&str> = call_overrides
568 .and_then(|o| o.client_factory.as_deref())
569 .or(global_client_factory);
570 let result_var = &call_config.result_var;
571 let args = &call_config.args;
572 let result_is_bytes_any_lang =
579 call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
580 eprintln!(
581 "[swift debug] fixture={} call={:?} result_is_bytes={} any_override_bytes={} overrides={}",
582 fixture.id,
583 fixture.call,
584 call_config.result_is_bytes,
585 call_config.overrides.values().any(|o| o.result_is_bytes),
586 call_config.overrides.len()
587 );
588 let result_is_simple = call_config.result_is_simple
589 || call_overrides.is_some_and(|o| o.result_is_simple)
590 || result_is_simple
591 || result_is_bytes_any_lang;
592 let result_is_array = call_config.result_is_array;
593 let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);
598
599 let method_name = fixture.id.to_upper_camel_case();
600 let description = &fixture.description;
601 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
602 let is_async = call_config.r#async;
603
604 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
606 let collect_snippet_opt = if is_streaming && !expects_error {
607 crate::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(lang, result_var, "chunks")
608 } else {
609 None
610 };
611 if is_streaming && !expects_error && collect_snippet_opt.is_none() {
618 if is_async {
619 let _ = writeln!(out, " func test{method_name}() async throws {{");
620 } else {
621 let _ = writeln!(out, " func test{method_name}() throws {{");
622 }
623 let _ = writeln!(out, " // {description}");
624 let _ = writeln!(
625 out,
626 " try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
627 fixture.id
628 );
629 let _ = writeln!(out, " }}");
630 return;
631 }
632 let collect_snippet = collect_snippet_opt.unwrap_or_default();
633
634 let has_unresolvable_json_object_arg = {
641 let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
642 options_via.is_none() && args.iter().any(|a| a.arg_type == "json_object" && a.name != "config")
643 };
644
645 if has_unresolvable_json_object_arg {
646 if is_async {
647 let _ = writeln!(out, " func test{method_name}() async throws {{");
648 } else {
649 let _ = writeln!(out, " func test{method_name}() throws {{");
650 }
651 let _ = writeln!(out, " // {description}");
652 let _ = writeln!(
653 out,
654 " try XCTSkipIf(true, \"swift: json_object request construction requires options_via configuration (fixture: {})\");",
655 fixture.id
656 );
657 let _ = writeln!(out, " }}");
658 return;
659 }
660
661 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
665
666 let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
671 let per_call = call_overrides.map(|o| &o.enum_fields);
672 if let Some(pc) = per_call {
673 if !pc.is_empty() {
674 let mut merged = enum_fields.clone();
675 merged.extend(pc.keys().cloned());
676 std::borrow::Cow::Owned(merged)
677 } else {
678 std::borrow::Cow::Borrowed(enum_fields)
679 }
680 } else {
681 std::borrow::Cow::Borrowed(enum_fields)
682 }
683 };
684
685 let options_via_str: Option<&str> = call_overrides.and_then(|o| o.options_via.as_deref());
686 let options_type_str: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
687 let handle_config_fn_owned: Option<String> = call_config
691 .overrides
692 .get("c")
693 .and_then(|c| c.c_engine_factory.as_deref())
694 .map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
695 let (setup_lines, args_str) = build_args_and_setup(
696 &fixture.input,
697 args,
698 &fixture.id,
699 fixture.has_host_root_route(),
700 &function_name,
701 options_via_str,
702 options_type_str,
703 handle_config_fn_owned.as_deref(),
704 );
705
706 let args_str = if extra_args.is_empty() {
708 args_str
709 } else if args_str.is_empty() {
710 extra_args.join(", ")
711 } else {
712 format!("{args_str}, {}", extra_args.join(", "))
713 };
714
715 let has_mock = fixture.mock_response.is_some();
720 let (call_setup, call_expr) = if let Some(_factory) = client_factory {
721 let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
722 let mock_url = if fixture.has_host_root_route() {
723 format!(
724 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\")",
725 fixture.id
726 )
727 } else {
728 format!(
729 "ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\"",
730 fixture.id
731 )
732 };
733 let client_constructor = if has_mock {
734 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
735 } else {
736 if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
738 format!(
739 "let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n \
740 let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n \
741 let _client = try DefaultClient(apiKey: _apiKey ?? \"test-key\", baseUrl: _baseUrl)"
742 )
743 } else {
744 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
745 }
746 };
747 let expr = if is_async {
748 format!("try await _client.{function_name}({args_str})")
749 } else {
750 format!("try _client.{function_name}({args_str})")
751 };
752 (Some(client_constructor), expr)
753 } else {
754 let expr = if is_async {
756 format!("try await {function_name}({args_str})")
757 } else {
758 format!("try {function_name}({args_str})")
759 };
760 (None, expr)
761 };
762 let _ = function_name;
764
765 if is_async {
766 let _ = writeln!(out, " func test{method_name}() async throws {{");
767 } else {
768 let _ = writeln!(out, " func test{method_name}() throws {{");
769 }
770 let _ = writeln!(out, " // {description}");
771
772 if expects_error {
773 if is_async {
777 let _ = writeln!(out, " do {{");
782 for line in &setup_lines {
783 let _ = writeln!(out, " {line}");
784 }
785 if let Some(setup) = &call_setup {
786 let _ = writeln!(out, " {setup}");
787 }
788 let _ = writeln!(out, " _ = {call_expr}");
789 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
790 let _ = writeln!(out, " }} catch {{");
791 let _ = writeln!(out, " // success");
792 let _ = writeln!(out, " }}");
793 } else {
794 let _ = writeln!(out, " do {{");
801 for line in &setup_lines {
802 let _ = writeln!(out, " {line}");
803 }
804 if let Some(setup) = &call_setup {
805 let _ = writeln!(out, " {setup}");
806 }
807 let _ = writeln!(out, " _ = {call_expr}");
808 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
809 let _ = writeln!(out, " }} catch {{");
810 let _ = writeln!(out, " // success");
811 let _ = writeln!(out, " }}");
812 }
813 let _ = writeln!(out, " }}");
814 return;
815 }
816
817 for line in &setup_lines {
818 let _ = writeln!(out, " {line}");
819 }
820
821 if let Some(setup) = &call_setup {
823 let _ = writeln!(out, " {setup}");
824 }
825
826 let _ = writeln!(out, " let {result_var} = {call_expr}");
827
828 if !collect_snippet.is_empty() {
831 for line in collect_snippet.lines() {
832 let _ = writeln!(out, " {line}");
833 }
834 }
835
836 for assertion in &fixture.assertions {
837 render_assertion(
838 out,
839 assertion,
840 result_var,
841 field_resolver,
842 result_is_simple,
843 result_is_array,
844 result_is_option,
845 &effective_enum_fields,
846 );
847 }
848
849 let _ = writeln!(out, " }}");
850}
851
852#[allow(clippy::too_many_arguments)]
853fn build_args_and_setup(
867 input: &serde_json::Value,
868 args: &[crate::config::ArgMapping],
869 fixture_id: &str,
870 has_host_root_route: bool,
871 function_name: &str,
872 options_via: Option<&str>,
873 options_type: Option<&str>,
874 handle_config_fn: Option<&str>,
875) -> (Vec<String>, String) {
876 if args.is_empty() {
877 return (Vec::new(), String::new());
878 }
879
880 let mut setup_lines: Vec<String> = Vec::new();
881 let mut parts: Vec<String> = Vec::new();
882
883 let later_emits: Vec<bool> = (0..args.len())
888 .map(|i| {
889 args.iter().skip(i + 1).any(|a| {
890 let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
891 let v = input.get(f);
892 let has_value = matches!(v, Some(x) if !x.is_null());
893 has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
894 })
895 })
896 .collect();
897
898 for (idx, arg) in args.iter().enumerate() {
899 if arg.arg_type == "mock_url" {
900 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
901 let url_expr = if has_host_root_route {
902 format!(
903 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\")"
904 )
905 } else {
906 format!("ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"")
907 };
908 setup_lines.push(format!("let {} = {url_expr}", arg.name));
909 parts.push(arg.name.clone());
910 continue;
911 }
912
913 if arg.arg_type == "handle" {
914 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
915 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
916 let config_val = input.get(field);
917 let has_config = config_val
918 .is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
919 if has_config {
920 if let Some(from_json_fn) = handle_config_fn {
921 let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
922 let escaped = escape_swift_str(&json_str);
923 let config_var = format!("{}Config", arg.name.to_lower_camel_case());
924 setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
925 setup_lines.push(format!("let {var_name} = try createEngine({config_var})"));
926 } else {
927 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
928 }
929 } else {
930 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
931 }
932 parts.push(var_name);
933 continue;
934 }
935
936 if arg.arg_type == "bytes" {
941 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
942 let val = input.get(field);
943 match val {
944 None | Some(serde_json::Value::Null) if arg.optional => {
945 if later_emits[idx] {
946 parts.push("nil".to_string());
947 }
948 }
949 None | Some(serde_json::Value::Null) => {
950 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
951 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
952 parts.push(var_name);
953 }
954 Some(serde_json::Value::String(s)) => {
955 let escaped = escape_swift(s);
956 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
957 let data_var = format!("{}Data", arg.name.to_lower_camel_case());
958 setup_lines.push(format!(
959 "let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
960 ));
961 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
962 setup_lines.push(format!("for _byte in {data_var} {{ {var_name}.push(value: _byte) }}"));
963 parts.push(var_name);
964 }
965 Some(serde_json::Value::Array(arr)) => {
966 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
967 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
968 for v in arr {
969 if let Some(n) = v.as_u64() {
970 setup_lines.push(format!("{var_name}.push(value: UInt8({n}))"));
971 }
972 }
973 parts.push(var_name);
974 }
975 Some(other) => {
976 let json_str = serde_json::to_string(other).unwrap_or_default();
978 let escaped = escape_swift(&json_str);
979 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
980 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
981 setup_lines.push(format!(
982 "for _byte in Array(\"{escaped}\".utf8) {{ {var_name}.push(value: _byte) }}"
983 ));
984 parts.push(var_name);
985 }
986 }
987 continue;
988 }
989
990 let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
995 let is_batch_fn = function_name.starts_with("batch") || function_name.starts_with("Batch");
996 if is_config_arg && !is_batch_fn {
997 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
998 let val = input.get(field);
999 let json_str = match val {
1000 None | Some(serde_json::Value::Null) => "{}".to_string(),
1001 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1002 };
1003 let escaped = escape_swift(&json_str);
1004 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1005 setup_lines.push(format!("let {var_name} = try extractionConfigFromJson(\"{escaped}\")"));
1006 parts.push(var_name);
1007 continue;
1008 }
1009
1010 if arg.arg_type == "json_object" && options_via == Some("from_json") {
1015 if let Some(type_name) = options_type {
1016 let resolved_val = super::resolve_field(input, &arg.field);
1017 let json_str = match resolved_val {
1018 serde_json::Value::Null => "{}".to_string(),
1019 v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1020 };
1021 let escaped = escape_swift(&json_str);
1022 let var_name = format!("_{}", arg.name.to_lower_camel_case());
1023 let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
1024 setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1025 parts.push(var_name);
1026 continue;
1027 }
1028 }
1029
1030 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1031 let val = input.get(field);
1032 match val {
1033 None | Some(serde_json::Value::Null) if arg.optional => {
1034 if later_emits[idx] {
1038 parts.push("nil".to_string());
1039 }
1040 }
1041 None | Some(serde_json::Value::Null) => {
1042 let default_val = match arg.arg_type.as_str() {
1043 "string" => "\"\"".to_string(),
1044 "int" | "integer" => "0".to_string(),
1045 "float" | "number" => "0.0".to_string(),
1046 "bool" | "boolean" => "false".to_string(),
1047 _ => "nil".to_string(),
1048 };
1049 parts.push(default_val);
1050 }
1051 Some(v) => {
1052 parts.push(json_to_swift(v));
1053 }
1054 }
1055 }
1056
1057 (setup_lines, parts.join(", "))
1058}
1059
1060#[allow(clippy::too_many_arguments)]
1061fn render_assertion(
1062 out: &mut String,
1063 assertion: &Assertion,
1064 result_var: &str,
1065 field_resolver: &FieldResolver,
1066 result_is_simple: bool,
1067 result_is_array: bool,
1068 result_is_option: bool,
1069 enum_fields: &HashSet<String>,
1070) {
1071 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1076 if let Some(f) = &assertion.field {
1079 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1080 if let Some(expr) =
1081 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "swift", "chunks")
1082 {
1083 let line = match assertion.assertion_type.as_str() {
1084 "count_min" => {
1085 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1086 format!(" XCTAssertGreaterThanOrEqual(chunks.count, {n})\n")
1087 } else {
1088 String::new()
1089 }
1090 }
1091 "count_equals" => {
1092 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1093 format!(" XCTAssertEqual(chunks.count, {n})\n")
1094 } else {
1095 String::new()
1096 }
1097 }
1098 "equals" => {
1099 if let Some(serde_json::Value::String(s)) = &assertion.value {
1100 let escaped = escape_swift(s);
1101 format!(" XCTAssertEqual({expr}, \"{escaped}\")\n")
1102 } else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
1103 format!(" XCTAssertEqual({expr}, {b})\n")
1104 } else {
1105 String::new()
1106 }
1107 }
1108 "not_empty" => {
1109 format!(" XCTAssertFalse({expr}.isEmpty, \"expected non-empty\")\n")
1110 }
1111 "is_empty" => {
1112 format!(" XCTAssertTrue({expr}.isEmpty, \"expected empty\")\n")
1113 }
1114 "is_true" => {
1115 format!(" XCTAssertTrue({expr})\n")
1116 }
1117 "is_false" => {
1118 format!(" XCTAssertFalse({expr})\n")
1119 }
1120 "greater_than" => {
1121 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1122 format!(" XCTAssertGreaterThan(chunks.count, {n})\n")
1123 } else {
1124 String::new()
1125 }
1126 }
1127 "contains" => {
1128 if let Some(serde_json::Value::String(s)) = &assertion.value {
1129 let escaped = escape_swift(s);
1130 format!(
1131 " XCTAssertTrue({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\")\n"
1132 )
1133 } else {
1134 String::new()
1135 }
1136 }
1137 _ => format!(
1138 " // streaming field '{f}': assertion type '{}' not rendered\n",
1139 assertion.assertion_type
1140 ),
1141 };
1142 if !line.is_empty() {
1143 out.push_str(&line);
1144 }
1145 }
1146 return;
1147 }
1148 }
1149
1150 if let Some(f) = &assertion.field {
1152 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1153 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1154 return;
1155 }
1156 }
1157
1158 if let Some(f) = &assertion.field {
1163 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1164 let _ = writeln!(
1165 out,
1166 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Swift)"
1167 );
1168 return;
1169 }
1170 }
1171
1172 let field_is_enum = assertion
1174 .field
1175 .as_deref()
1176 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1177
1178 let field_is_optional = assertion.field.as_deref().is_some_and(|f| {
1179 !f.is_empty() && (field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f)))
1180 });
1181 let field_is_array = assertion.field.as_deref().is_some_and(|f| {
1182 !f.is_empty()
1183 && (field_resolver.is_array(f)
1184 || field_resolver.is_array(field_resolver.resolve(f))
1185 || field_resolver.is_collection_root(f)
1186 || field_resolver.is_collection_root(field_resolver.resolve(f)))
1187 });
1188
1189 let field_expr_raw = if result_is_simple {
1190 result_var.to_string()
1191 } else {
1192 match &assertion.field {
1193 Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
1194 _ => result_var.to_string(),
1195 }
1196 };
1197
1198 let local_suffix = {
1208 use std::hash::{Hash, Hasher};
1209 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1210 assertion.field.hash(&mut hasher);
1211 assertion
1212 .value
1213 .as_ref()
1214 .map(|v| v.to_string())
1215 .unwrap_or_default()
1216 .hash(&mut hasher);
1217 format!(
1218 "{}_{:x}",
1219 assertion.assertion_type.replace(['-', '.'], "_"),
1220 hasher.finish() & 0xffff_ffff,
1221 )
1222 };
1223 let (vec_setup, field_expr) = materialise_vec_temporaries(&field_expr_raw, &local_suffix);
1224 let field_uses_traversal = assertion.field.as_deref().is_some_and(|f| f.contains("[]."));
1229 let traversal_skips_field_expr = field_uses_traversal
1230 && matches!(
1231 assertion.assertion_type.as_str(),
1232 "contains" | "not_contains" | "not_empty" | "is_empty"
1233 );
1234 if !traversal_skips_field_expr {
1235 for line in &vec_setup {
1236 let _ = writeln!(out, " {line}");
1237 }
1238 }
1239
1240 let accessor_is_optional = field_expr.contains("?.");
1246
1247 let string_expr = if field_is_enum && (field_is_optional || accessor_is_optional) {
1256 format!("({field_expr}?.toString() ?? \"\")")
1259 } else if field_is_enum {
1260 format!("{field_expr}.toString()")
1265 } else if field_is_optional {
1266 format!("({field_expr}?.toString() ?? \"\")")
1268 } else if accessor_is_optional {
1269 format!("({field_expr}.toString() ?? \"\")")
1272 } else {
1273 format!("{field_expr}.toString()")
1274 };
1275
1276 match assertion.assertion_type.as_str() {
1277 "equals" => {
1278 if let Some(expected) = &assertion.value {
1279 let swift_val = json_to_swift(expected);
1280 if expected.is_string() {
1281 if field_is_enum {
1282 let trim_expr = format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespaces)");
1286 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1287 } else {
1288 let trim_expr = format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespaces)");
1293 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1294 }
1295 } else {
1296 let _ = writeln!(out, " XCTAssertEqual({field_expr}, {swift_val})");
1297 }
1298 }
1299 }
1300 "contains" => {
1301 if let Some(expected) = &assertion.value {
1302 let swift_val = json_to_swift(expected);
1303 let no_field = assertion.field.as_deref().is_none_or(|f| f.is_empty());
1306 if result_is_simple && result_is_array && no_field {
1307 let _ = writeln!(
1310 out,
1311 " XCTAssertTrue({result_var}.map {{ $0.as_str().toString() }}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1312 );
1313 } else {
1314 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1316 if let Some(dot) = f.find("[].") {
1317 let array_part = &f[..dot];
1318 let elem_part = &f[dot + 3..];
1319 let line = swift_traversal_contains_assert(
1320 array_part,
1321 elem_part,
1322 f,
1323 &swift_val,
1324 result_var,
1325 false,
1326 &format!("expected to contain: \\({swift_val})"),
1327 enum_fields,
1328 field_resolver,
1329 );
1330 let _ = writeln!(out, "{line}");
1331 true
1332 } else {
1333 false
1334 }
1335 } else {
1336 false
1337 };
1338 if !traversal_handled {
1339 let field_is_array = assertion
1341 .field
1342 .as_deref()
1343 .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
1344 if field_is_array {
1345 let contains_expr =
1346 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1347 let _ = writeln!(
1348 out,
1349 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1350 );
1351 } else if field_is_enum {
1352 let _ = writeln!(
1355 out,
1356 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1357 );
1358 } else {
1359 let _ = writeln!(
1360 out,
1361 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1362 );
1363 }
1364 }
1365 }
1366 }
1367 }
1368 "contains_all" => {
1369 if let Some(values) = &assertion.values {
1370 if let Some(f) = assertion.field.as_deref() {
1372 if let Some(dot) = f.find("[].") {
1373 let array_part = &f[..dot];
1374 let elem_part = &f[dot + 3..];
1375 for val in values {
1376 let swift_val = json_to_swift(val);
1377 let line = swift_traversal_contains_assert(
1378 array_part,
1379 elem_part,
1380 f,
1381 &swift_val,
1382 result_var,
1383 false,
1384 &format!("expected to contain: \\({swift_val})"),
1385 enum_fields,
1386 field_resolver,
1387 );
1388 let _ = writeln!(out, "{line}");
1389 }
1390 } else {
1392 let field_is_array = field_resolver.is_array(field_resolver.resolve(f));
1394 if field_is_array {
1395 let contains_expr =
1396 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1397 for val in values {
1398 let swift_val = json_to_swift(val);
1399 let _ = writeln!(
1400 out,
1401 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1402 );
1403 }
1404 } else if field_is_enum {
1405 for val in values {
1408 let swift_val = json_to_swift(val);
1409 let _ = writeln!(
1410 out,
1411 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1412 );
1413 }
1414 } else {
1415 for val in values {
1416 let swift_val = json_to_swift(val);
1417 let _ = writeln!(
1418 out,
1419 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1420 );
1421 }
1422 }
1423 }
1424 } else {
1425 for val in values {
1427 let swift_val = json_to_swift(val);
1428 let _ = writeln!(
1429 out,
1430 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1431 );
1432 }
1433 }
1434 }
1435 }
1436 "not_contains" => {
1437 if let Some(expected) = &assertion.value {
1438 let swift_val = json_to_swift(expected);
1439 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1441 if let Some(dot) = f.find("[].") {
1442 let array_part = &f[..dot];
1443 let elem_part = &f[dot + 3..];
1444 let line = swift_traversal_contains_assert(
1445 array_part,
1446 elem_part,
1447 f,
1448 &swift_val,
1449 result_var,
1450 true,
1451 &format!("expected NOT to contain: \\({swift_val})"),
1452 enum_fields,
1453 field_resolver,
1454 );
1455 let _ = writeln!(out, "{line}");
1456 true
1457 } else {
1458 false
1459 }
1460 } else {
1461 false
1462 };
1463 if !traversal_handled {
1464 let _ = writeln!(
1465 out,
1466 " XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
1467 );
1468 }
1469 }
1470 }
1471 "not_empty" => {
1472 let traversal_not_empty_handled = if let Some(f) = assertion.field.as_deref() {
1479 if let Some(dot) = f.find("[].") {
1480 let array_part = &f[..dot];
1481 let elem_part = &f[dot + 3..];
1482 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1483 let resolved_full = field_resolver.resolve(f);
1484 let resolved_elem_part = resolved_full
1485 .find("[].")
1486 .map(|d| &resolved_full[d + 3..])
1487 .unwrap_or(elem_part);
1488 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1489 let elem_is_enum = enum_fields.contains(f) || enum_fields.contains(resolved_full);
1490 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1491 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1492 let elem_str = if elem_is_enum {
1493 format!("{elem_accessor}.to_string().toString()")
1494 } else if elem_is_optional {
1495 format!("({elem_accessor}?.toString() ?? \"\")")
1496 } else {
1497 format!("{elem_accessor}.toString()")
1498 };
1499 let _ = writeln!(
1500 out,
1501 " XCTAssertTrue({array_accessor}.contains(where: {{ !{elem_str}.isEmpty }}), \"expected non-empty value\")"
1502 );
1503 true
1504 } else {
1505 false
1506 }
1507 } else {
1508 false
1509 };
1510 if !traversal_not_empty_handled {
1511 if bare_result_is_option {
1512 let _ = writeln!(out, " XCTAssertNotNil({result_var}, \"expected non-nil value\")");
1513 } else if field_is_optional {
1514 let _ = writeln!(out, " XCTAssertNotNil({field_expr}, \"expected non-nil value\")");
1515 } else if field_is_array {
1516 let _ = writeln!(
1517 out,
1518 " XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
1519 );
1520 } else if result_is_simple {
1521 let _ = writeln!(
1523 out,
1524 " XCTAssertFalse({result_var}.isEmpty, \"expected non-empty value\")"
1525 );
1526 } else {
1527 let len_expr = if accessor_is_optional {
1536 format!("({field_expr}.len() ?? 0)")
1537 } else {
1538 format!("{field_expr}.len()")
1539 };
1540 let _ = writeln!(
1541 out,
1542 " XCTAssertGreaterThan({len_expr}, 0, \"expected non-empty value\")"
1543 );
1544 }
1545 }
1546 }
1547 "is_empty" => {
1548 if bare_result_is_option {
1549 let _ = writeln!(out, " XCTAssertNil({result_var}, \"expected nil value\")");
1550 } else if field_is_optional {
1551 let _ = writeln!(out, " XCTAssertNil({field_expr}, \"expected nil value\")");
1552 } else if field_is_array {
1553 let _ = writeln!(
1554 out,
1555 " XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
1556 );
1557 } else {
1558 let len_expr = if accessor_is_optional {
1562 format!("({field_expr}.len() ?? 0)")
1563 } else {
1564 format!("{field_expr}.len()")
1565 };
1566 let _ = writeln!(out, " XCTAssertEqual({len_expr}, 0, \"expected empty value\")");
1567 }
1568 }
1569 "contains_any" => {
1570 if let Some(values) = &assertion.values {
1571 let checks: Vec<String> = values
1572 .iter()
1573 .map(|v| {
1574 let swift_val = json_to_swift(v);
1575 format!("{string_expr}.contains({swift_val})")
1576 })
1577 .collect();
1578 let joined = checks.join(" || ");
1579 let _ = writeln!(
1580 out,
1581 " XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
1582 );
1583 }
1584 }
1585 "greater_than" => {
1586 if let Some(val) = &assertion.value {
1587 let swift_val = json_to_swift(val);
1588 let field_is_optional = accessor_is_optional
1591 || assertion.field.as_deref().is_some_and(|f| {
1592 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1593 });
1594 let compare_expr = if field_is_optional {
1595 format!("({field_expr} ?? 0)")
1596 } else {
1597 field_expr.clone()
1598 };
1599 let _ = writeln!(out, " XCTAssertGreaterThan({compare_expr}, {swift_val})");
1600 }
1601 }
1602 "less_than" => {
1603 if let Some(val) = &assertion.value {
1604 let swift_val = json_to_swift(val);
1605 let field_is_optional = accessor_is_optional
1606 || assertion.field.as_deref().is_some_and(|f| {
1607 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1608 });
1609 let compare_expr = if field_is_optional {
1610 format!("({field_expr} ?? 0)")
1611 } else {
1612 field_expr.clone()
1613 };
1614 let _ = writeln!(out, " XCTAssertLessThan({compare_expr}, {swift_val})");
1615 }
1616 }
1617 "greater_than_or_equal" => {
1618 if let Some(val) = &assertion.value {
1619 let swift_val = json_to_swift(val);
1620 let field_is_optional = accessor_is_optional
1623 || assertion.field.as_deref().is_some_and(|f| {
1624 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1625 });
1626 let compare_expr = if field_is_optional {
1627 format!("({field_expr} ?? 0)")
1628 } else {
1629 field_expr.clone()
1630 };
1631 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({compare_expr}, {swift_val})");
1632 }
1633 }
1634 "less_than_or_equal" => {
1635 if let Some(val) = &assertion.value {
1636 let swift_val = json_to_swift(val);
1637 let field_is_optional = accessor_is_optional
1638 || assertion.field.as_deref().is_some_and(|f| {
1639 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1640 });
1641 let compare_expr = if field_is_optional {
1642 format!("({field_expr} ?? 0)")
1643 } else {
1644 field_expr.clone()
1645 };
1646 let _ = writeln!(out, " XCTAssertLessThanOrEqual({compare_expr}, {swift_val})");
1647 }
1648 }
1649 "starts_with" => {
1650 if let Some(expected) = &assertion.value {
1651 let swift_val = json_to_swift(expected);
1652 let _ = writeln!(
1653 out,
1654 " XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
1655 );
1656 }
1657 }
1658 "ends_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}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
1664 );
1665 }
1666 }
1667 "min_length" => {
1668 if let Some(val) = &assertion.value {
1669 if let Some(n) = val.as_u64() {
1670 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({string_expr}.count, {n})");
1673 }
1674 }
1675 }
1676 "max_length" => {
1677 if let Some(val) = &assertion.value {
1678 if let Some(n) = val.as_u64() {
1679 let _ = writeln!(out, " XCTAssertLessThanOrEqual({string_expr}.count, {n})");
1680 }
1681 }
1682 }
1683 "count_min" => {
1684 if let Some(val) = &assertion.value {
1685 if let Some(n) = val.as_u64() {
1686 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1690 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({count_expr}, {n})");
1691 }
1692 }
1693 }
1694 "count_equals" => {
1695 if let Some(val) = &assertion.value {
1696 if let Some(n) = val.as_u64() {
1697 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1698 let _ = writeln!(out, " XCTAssertEqual({count_expr}, {n})");
1699 }
1700 }
1701 }
1702 "is_true" => {
1703 let _ = writeln!(out, " XCTAssertTrue({field_expr})");
1704 }
1705 "is_false" => {
1706 let _ = writeln!(out, " XCTAssertFalse({field_expr})");
1707 }
1708 "matches_regex" => {
1709 if let Some(expected) = &assertion.value {
1710 let swift_val = json_to_swift(expected);
1711 let _ = writeln!(
1712 out,
1713 " XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
1714 );
1715 }
1716 }
1717 "not_error" => {
1718 }
1720 "error" => {
1721 }
1723 "method_result" => {
1724 let _ = writeln!(out, " // method_result assertions not yet implemented for Swift");
1725 }
1726 other => {
1727 panic!("Swift e2e generator: unsupported assertion type: {other}");
1728 }
1729 }
1730}
1731
1732fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String) {
1753 let Some(idx) = expr.find("()[") else {
1754 return (Vec::new(), expr.to_string());
1755 };
1756 let after_open = idx + 3; let Some(close_rel) = expr[after_open..].find(']') else {
1758 return (Vec::new(), expr.to_string());
1759 };
1760 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);
1765 let method = &expr[method_dot + 1..idx];
1766 let local = format!("_vec_{}_{}", method, name_suffix);
1767 let setup = format!("let {local} = {prefix}");
1768 let rewritten = format!("{local}{subscript}{tail}");
1769 (vec![setup], rewritten)
1770}
1771
1772fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
1775 let resolved = field_resolver.resolve(field);
1776 let parts: Vec<&str> = resolved.split('.').collect();
1777
1778 let mut out = result_var.to_string();
1781 let mut has_optional = false;
1782 let mut path_so_far = String::new();
1783 let total = parts.len();
1784 for (i, part) in parts.iter().enumerate() {
1785 let is_leaf = i == total - 1;
1786 let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
1790 (&part[..bracket_pos], Some(&part[bracket_pos..]))
1791 } else {
1792 (part, None)
1793 };
1794
1795 if !path_so_far.is_empty() {
1796 path_so_far.push('.');
1797 }
1798 let base_path = {
1802 let mut p = path_so_far.clone();
1803 p.push_str(field_name);
1804 p
1805 };
1806 path_so_far.push_str(part);
1809
1810 out.push('.');
1811 out.push_str(field_name);
1812 if let Some(sub) = subscript {
1813 let field_is_optional = field_resolver.is_optional(&base_path);
1817 if field_is_optional {
1818 out.push_str("()?");
1819 has_optional = true;
1820 } else {
1821 out.push_str("()");
1822 }
1823 out.push_str(sub);
1824 } else {
1834 out.push_str("()");
1835 if !is_leaf && field_resolver.is_optional(&base_path) {
1838 out.push('?');
1839 has_optional = true;
1840 }
1841 }
1842 }
1843 (out, has_optional)
1844}
1845
1846#[allow(clippy::too_many_arguments)]
1868fn swift_traversal_contains_assert(
1869 array_part: &str,
1870 element_part: &str,
1871 full_field: &str,
1872 val_expr: &str,
1873 result_var: &str,
1874 negate: bool,
1875 msg: &str,
1876 enum_fields: &std::collections::HashSet<String>,
1877 field_resolver: &FieldResolver,
1878) -> String {
1879 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1880 let resolved_full = field_resolver.resolve(full_field);
1881 let resolved_elem_part = resolved_full
1882 .find("[].")
1883 .map(|d| &resolved_full[d + 3..])
1884 .unwrap_or(element_part);
1885 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1886 let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
1887 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1888 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1889 let elem_str = if elem_is_enum {
1890 format!("{elem_accessor}.toString()")
1893 } else if elem_is_optional {
1894 format!("({elem_accessor}?.toString() ?? \"\")")
1895 } else {
1896 format!("{elem_accessor}.toString()")
1897 };
1898 let assert_fn = if negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
1899 format!(" {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
1900}
1901
1902fn swift_array_contains_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
1903 let Some(f) = field else {
1904 return format!("{result_var}.map {{ $0.as_str().toString() }}");
1905 };
1906 let (accessor, _has_optional) = swift_build_accessor(f, result_var, field_resolver);
1907 format!("{accessor}?.map {{ $0.as_str().toString() }}")
1910}
1911
1912fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
1921 let Some(f) = field else {
1922 return format!("{result_var}.count");
1923 };
1924 let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
1925 if field_resolver.is_optional(f) {
1927 has_optional = true;
1928 }
1929 if has_optional {
1930 if accessor.contains("?.") {
1933 format!("{accessor}.count ?? 0")
1934 } else {
1935 format!("({accessor}?.count ?? 0)")
1938 }
1939 } else {
1940 format!("{accessor}.count")
1941 }
1942}
1943
1944fn json_to_swift(value: &serde_json::Value) -> String {
1946 match value {
1947 serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
1948 serde_json::Value::Bool(b) => b.to_string(),
1949 serde_json::Value::Number(n) => n.to_string(),
1950 serde_json::Value::Null => "nil".to_string(),
1951 serde_json::Value::Array(arr) => {
1952 let items: Vec<String> = arr.iter().map(json_to_swift).collect();
1953 format!("[{}]", items.join(", "))
1954 }
1955 serde_json::Value::Object(_) => {
1956 let json_str = serde_json::to_string(value).unwrap_or_default();
1957 format!("\"{}\"", escape_swift(&json_str))
1958 }
1959 }
1960}
1961
1962fn escape_swift(s: &str) -> String {
1964 escape_swift_str(s)
1965}
1966
1967#[cfg(test)]
1968mod tests {
1969 use super::*;
1970 use crate::field_access::FieldResolver;
1971 use std::collections::{HashMap, HashSet};
1972
1973 fn make_resolver_tool_calls() -> FieldResolver {
1974 let mut optional = HashSet::new();
1978 optional.insert("choices.message.tool_calls".to_string());
1979 let mut arrays = HashSet::new();
1980 arrays.insert("choices".to_string());
1981 FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
1982 }
1983
1984 #[test]
1991 fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
1992 let resolver = make_resolver_tool_calls();
1993 let (accessor, has_optional) =
1996 swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
1997 assert!(
2000 accessor.contains("tool_calls()?[0]"),
2001 "expected `tool_calls()?[0]` for optional tool_calls, got: {accessor}"
2002 );
2003 assert!(
2005 !accessor.contains("?[0]?"),
2006 "must not emit trailing `?` after subscript index: {accessor}"
2007 );
2008 assert!(has_optional, "expected has_optional=true for optional field chain");
2010 assert!(
2012 accessor.contains("[0].function()"),
2013 "expected `.function()` (non-optional) after subscript: {accessor}"
2014 );
2015 }
2016}