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