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