1use crate::config::E2eConfig;
16use crate::escape::{escape_java as escape_swift_str, expand_fixture_templates, sanitize_filename, sanitize_ident};
17use crate::field_access::{FieldResolver, SwiftFirstClassMap};
18use crate::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
19use alef_core::backend::GeneratedFile;
20use alef_core::config::ResolvedCrateConfig;
21use alef_core::hash::{self, CommentStyle};
22use alef_core::template_versions::toolchain;
23use anyhow::Result;
24use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
25use std::collections::HashMap;
26use std::collections::HashSet;
27use std::fmt::Write as FmtWrite;
28use std::path::PathBuf;
29
30use super::E2eCodegen;
31use super::client;
32
33pub struct SwiftE2eCodegen;
35
36impl E2eCodegen for SwiftE2eCodegen {
37 fn generate(
38 &self,
39 groups: &[FixtureGroup],
40 e2e_config: &E2eConfig,
41 config: &ResolvedCrateConfig,
42 type_defs: &[alef_core::ir::TypeDef],
43 _enums: &[alef_core::ir::EnumDef],
44 ) -> Result<Vec<GeneratedFile>> {
45 let lang = self.language_name();
46 let output_base = PathBuf::from(e2e_config.effective_output()).join("swift_e2e");
54
55 let mut files = Vec::new();
56
57 let call = &e2e_config.call;
59 let overrides = call.overrides.get(lang);
60 let function_name = overrides
61 .and_then(|o| o.function.as_ref())
62 .cloned()
63 .unwrap_or_else(|| call.function.clone());
64 let result_var = &call.result_var;
65 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
66
67 let swift_pkg = e2e_config.resolve_package("swift");
69 let pkg_name = swift_pkg
70 .as_ref()
71 .and_then(|p| p.name.as_ref())
72 .cloned()
73 .unwrap_or_else(|| config.name.to_upper_camel_case());
74 let pkg_path = swift_pkg
75 .as_ref()
76 .and_then(|p| p.path.as_ref())
77 .cloned()
78 .unwrap_or_else(|| "../../packages/swift".to_string());
79 let pkg_version = swift_pkg
80 .as_ref()
81 .and_then(|p| p.version.as_ref())
82 .cloned()
83 .or_else(|| config.resolved_version())
84 .unwrap_or_else(|| "0.1.0".to_string());
85
86 let module_name = pkg_name.as_str();
88
89 let registry_url = config
93 .try_github_repo()
94 .map(|repo| {
95 let base = repo.trim_end_matches('/').trim_end_matches(".git");
96 format!("{base}.git")
97 })
98 .unwrap_or_else(|_| format!("https://example.invalid/{module_name}.git"));
99
100 files.push(GeneratedFile {
103 path: output_base.join("Package.swift"),
104 content: render_package_swift(module_name, ®istry_url, &pkg_path, &pkg_version, e2e_config.dep_mode),
105 generated_header: false,
106 });
107
108 let tests_base = output_base.clone();
110
111 let swift_first_class_map = build_swift_first_class_map(type_defs, e2e_config);
117
118 let field_resolver = FieldResolver::new_with_swift_first_class(
119 &e2e_config.fields,
120 &e2e_config.fields_optional,
121 &e2e_config.result_fields,
122 &e2e_config.fields_array,
123 &e2e_config.fields_method_calls,
124 &HashMap::new(),
125 swift_first_class_map,
126 );
127
128 let client_factory: Option<&str> = overrides.and_then(|o| o.client_factory.as_deref());
130
131 files.push(GeneratedFile {
142 path: tests_base
143 .join("Tests")
144 .join(format!("{module_name}E2ETests"))
145 .join("TestHelpers.swift"),
146 content: render_test_helpers_swift(),
147 generated_header: true,
148 });
149
150 for group in groups {
152 let active: Vec<&Fixture> = group
153 .fixtures
154 .iter()
155 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
156 .collect();
157
158 if active.is_empty() {
159 continue;
160 }
161
162 let class_name = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
163 let filename = format!("{class_name}.swift");
164 let content = render_test_file(
165 &group.category,
166 &active,
167 e2e_config,
168 module_name,
169 &class_name,
170 &function_name,
171 result_var,
172 &e2e_config.call.args,
173 &field_resolver,
174 result_is_simple,
175 &e2e_config.fields_enum,
176 client_factory,
177 );
178 files.push(GeneratedFile {
179 path: tests_base
180 .join("Tests")
181 .join(format!("{module_name}E2ETests"))
182 .join(filename),
183 content,
184 generated_header: true,
185 });
186 }
187
188 Ok(files)
189 }
190
191 fn language_name(&self) -> &'static str {
192 "swift"
193 }
194}
195
196fn render_test_helpers_swift() -> String {
205 let header = hash::header(CommentStyle::DoubleSlash);
206 format!(
207 r#"{header}import Foundation
208import RustBridge
209
210// Make `RustString` print its content in XCTest failure output. Without this,
211// every error thrown from the swift-bridge layer surfaces as
212// `caught error: "RustBridge.RustString"` with the actual message hidden
213// inside the opaque class instance. The `@retroactive` keyword acknowledges
214// that the conformed-to protocol (`CustomStringConvertible`) and the
215// conforming type (`RustString`) both live outside this module — required by
216// Swift 6 to silence the retroactive-conformance warning. swift-bridge does
217// not give `RustString` a `description` of its own, so there is no conflict.
218extension RustString: @retroactive CustomStringConvertible {{
219 public var description: String {{ self.toString() }}
220}}
221"#
222 )
223}
224
225fn render_package_swift(
226 module_name: &str,
227 registry_url: &str,
228 pkg_path: &str,
229 pkg_version: &str,
230 dep_mode: crate::config::DependencyMode,
231) -> String {
232 let min_macos = toolchain::SWIFT_MIN_MACOS;
233
234 let (dep_block, product_dep) = match dep_mode {
238 crate::config::DependencyMode::Registry => {
239 let dep = format!(r#" .package(url: "{registry_url}", from: "{pkg_version}")"#);
240 let pkg_id = registry_url
241 .trim_end_matches('/')
242 .trim_end_matches(".git")
243 .split('/')
244 .next_back()
245 .unwrap_or(module_name);
246 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
247 (dep, prod)
248 }
249 crate::config::DependencyMode::Local => {
250 let pkg_id = pkg_path.trim_end_matches('/').rsplit('/').next().unwrap_or(module_name);
257 let dep = format!(r#" .package(path: "{pkg_path}")"#);
258 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
259 (dep, prod)
260 }
261 };
262 let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
265 let min_ios = toolchain::SWIFT_MIN_IOS;
266 let min_ios_major = min_ios.split('.').next().unwrap_or(min_ios);
267 format!(
271 r#"// swift-tools-version: 6.0
272import PackageDescription
273
274let package = Package(
275 name: "E2eSwift",
276 platforms: [
277 .macOS(.v{min_macos_major}),
278 .iOS(.v{min_ios_major}),
279 ],
280 dependencies: [
281{dep_block},
282 ],
283 targets: [
284 .testTarget(
285 name: "{module_name}E2ETests",
286 dependencies: [{product_dep}]
287 ),
288 ]
289)
290"#
291 )
292}
293
294#[allow(clippy::too_many_arguments)]
295fn render_test_file(
296 category: &str,
297 fixtures: &[&Fixture],
298 e2e_config: &E2eConfig,
299 module_name: &str,
300 class_name: &str,
301 function_name: &str,
302 result_var: &str,
303 args: &[crate::config::ArgMapping],
304 field_resolver: &FieldResolver,
305 result_is_simple: bool,
306 enum_fields: &HashSet<String>,
307 client_factory: Option<&str>,
308) -> String {
309 let needs_chdir = fixtures.iter().any(|f| {
316 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
317 call_config
318 .args
319 .iter()
320 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
321 });
322
323 let mut out = String::new();
324 out.push_str(&hash::header(CommentStyle::DoubleSlash));
325 let _ = writeln!(out, "import XCTest");
326 let _ = writeln!(out, "import Foundation");
327 let _ = writeln!(out, "import {module_name}");
328 let _ = writeln!(out, "import RustBridge");
329 let _ = writeln!(out);
330 let _ = writeln!(out, "/// E2e tests for category: {category}.");
331 let _ = writeln!(out, "final class {class_name}: XCTestCase {{");
332
333 if needs_chdir {
334 let _ = writeln!(out, " override class func setUp() {{");
342 let _ = writeln!(out, " super.setUp()");
343 let _ = writeln!(out, " let _testDocs = URL(fileURLWithPath: #filePath)");
344 let _ = writeln!(out, " .deletingLastPathComponent() // <Module>Tests/");
345 let _ = writeln!(out, " .deletingLastPathComponent() // Tests/");
346 let _ = writeln!(out, " .deletingLastPathComponent() // swift/");
347 let _ = writeln!(out, " .deletingLastPathComponent() // packages/");
348 let _ = writeln!(out, " .deletingLastPathComponent() // <repo root>");
349 let _ = writeln!(
350 out,
351 " .appendingPathComponent(\"{}\")",
352 e2e_config.test_documents_dir
353 );
354 let _ = writeln!(
355 out,
356 " if FileManager.default.fileExists(atPath: _testDocs.path) {{"
357 );
358 let _ = writeln!(
359 out,
360 " FileManager.default.changeCurrentDirectoryPath(_testDocs.path)"
361 );
362 let _ = writeln!(out, " }}");
363 let _ = writeln!(out, " }}");
364 let _ = writeln!(out);
365 }
366
367 for fixture in fixtures {
368 if fixture.is_http_test() {
369 render_http_test_method(&mut out, fixture);
370 } else {
371 render_test_method(
372 &mut out,
373 fixture,
374 e2e_config,
375 function_name,
376 result_var,
377 args,
378 field_resolver,
379 result_is_simple,
380 enum_fields,
381 client_factory,
382 );
383 }
384 let _ = writeln!(out);
385 }
386
387 let _ = writeln!(out, "}}");
388 out
389}
390
391struct SwiftTestClientRenderer;
398
399impl client::TestClientRenderer for SwiftTestClientRenderer {
400 fn language_name(&self) -> &'static str {
401 "swift"
402 }
403
404 fn sanitize_test_name(&self, id: &str) -> String {
405 sanitize_ident(id).to_upper_camel_case()
407 }
408
409 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
415 let _ = writeln!(out, " /// {description}");
416 let _ = writeln!(out, " func test{fn_name}() throws {{");
417 if let Some(reason) = skip_reason {
418 let escaped = escape_swift(reason);
419 let _ = writeln!(out, " try XCTSkipIf(true, \"{escaped}\")");
420 }
421 }
422
423 fn render_test_close(&self, out: &mut String) {
424 let _ = writeln!(out, " }}");
425 }
426
427 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
434 let method = ctx.method.to_uppercase();
435 let fixture_path = escape_swift(ctx.path);
436
437 let _ = writeln!(
438 out,
439 " let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
440 );
441 let _ = writeln!(
442 out,
443 " var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
444 );
445 let _ = writeln!(out, " _req.httpMethod = \"{method}\"");
446
447 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
449 header_pairs.sort_by_key(|(k, _)| k.as_str());
450 for (k, v) in &header_pairs {
451 let expanded_v = expand_fixture_templates(v);
452 let ek = escape_swift(k);
453 let ev = escape_swift(&expanded_v);
454 let _ = writeln!(out, " _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
455 }
456
457 if let Some(body) = ctx.body {
459 let json_str = serde_json::to_string(body).unwrap_or_default();
460 let escaped_body = escape_swift(&json_str);
461 let _ = writeln!(out, " _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
462 let _ = writeln!(
463 out,
464 " _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
465 );
466 }
467
468 let _ = writeln!(out, " var {}: HTTPURLResponse?", ctx.response_var);
469 let _ = writeln!(out, " var _responseData: Data?");
470 let _ = writeln!(out, " let _sema = DispatchSemaphore(value: 0)");
471 let _ = writeln!(
472 out,
473 " URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
474 );
475 let _ = writeln!(out, " {} = resp as? HTTPURLResponse", ctx.response_var);
476 let _ = writeln!(out, " _responseData = data");
477 let _ = writeln!(out, " _sema.signal()");
478 let _ = writeln!(out, " }}.resume()");
479 let _ = writeln!(out, " _sema.wait()");
480 let _ = writeln!(out, " let _resp = try XCTUnwrap({})", ctx.response_var);
481 }
482
483 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
484 let _ = writeln!(out, " XCTAssertEqual(_resp.statusCode, {status})");
485 }
486
487 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
488 let lower_name = name.to_lowercase();
489 let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
490 match expected {
491 "<<present>>" => {
492 let _ = writeln!(out, " XCTAssertNotNil({header_expr})");
493 }
494 "<<absent>>" => {
495 let _ = writeln!(out, " XCTAssertNil({header_expr})");
496 }
497 "<<uuid>>" => {
498 let _ = writeln!(out, " let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
499 let _ = writeln!(
500 out,
501 " 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))"
502 );
503 }
504 exact => {
505 let escaped = escape_swift(exact);
506 let _ = writeln!(out, " XCTAssertEqual({header_expr}, \"{escaped}\")");
507 }
508 }
509 }
510
511 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
512 if let serde_json::Value::String(s) = expected {
513 let escaped = escape_swift(s);
514 let _ = writeln!(
515 out,
516 " let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
517 );
518 let _ = writeln!(
519 out,
520 " XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
521 );
522 } else {
523 let json_str = serde_json::to_string(expected).unwrap_or_default();
524 let escaped = escape_swift(&json_str);
525 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
526 let _ = writeln!(
527 out,
528 " let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
529 );
530 let _ = writeln!(
531 out,
532 " let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
533 );
534 let _ = writeln!(
535 out,
536 " XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
537 );
538 }
539 }
540
541 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
542 if let Some(obj) = expected.as_object() {
543 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
544 let _ = writeln!(
545 out,
546 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
547 );
548 for (key, val) in obj {
549 let escaped_key = escape_swift(key);
550 let swift_val = json_to_swift(val);
551 let _ = writeln!(
552 out,
553 " XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
554 );
555 }
556 }
557 }
558
559 fn render_assert_validation_errors(
560 &self,
561 out: &mut String,
562 _response_var: &str,
563 errors: &[ValidationErrorExpectation],
564 ) {
565 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
566 let _ = writeln!(
567 out,
568 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
569 );
570 let _ = writeln!(
571 out,
572 " let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
573 );
574 for ve in errors {
575 let escaped_msg = escape_swift(&ve.msg);
576 let _ = writeln!(
577 out,
578 " XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
579 );
580 }
581 }
582}
583
584fn render_http_test_method(out: &mut String, fixture: &Fixture) {
589 let Some(http) = &fixture.http else {
590 return;
591 };
592
593 if http.expected_response.status_code == 101 {
595 let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
596 let description = fixture.description.replace('"', "\\\"");
597 let _ = writeln!(out, " /// {description}");
598 let _ = writeln!(out, " func test{method_name}() throws {{");
599 let _ = writeln!(
600 out,
601 " try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
602 );
603 let _ = writeln!(out, " }}");
604 return;
605 }
606
607 client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
608}
609
610#[allow(clippy::too_many_arguments)]
615fn render_test_method(
616 out: &mut String,
617 fixture: &Fixture,
618 e2e_config: &E2eConfig,
619 _function_name: &str,
620 _result_var: &str,
621 _args: &[crate::config::ArgMapping],
622 field_resolver: &FieldResolver,
623 result_is_simple: bool,
624 enum_fields: &HashSet<String>,
625 global_client_factory: Option<&str>,
626) {
627 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
629 let lang = "swift";
630 let call_overrides = call_config.overrides.get(lang);
631 let function_name = call_overrides
632 .and_then(|o| o.function.as_ref())
633 .cloned()
634 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
635 let client_factory: Option<&str> = call_overrides
637 .and_then(|o| o.client_factory.as_deref())
638 .or(global_client_factory);
639 let result_var = &call_config.result_var;
640 let args = &call_config.args;
641 let result_is_bytes_any_lang =
648 call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
649 let result_is_simple = call_config.result_is_simple
650 || call_overrides.is_some_and(|o| o.result_is_simple)
651 || result_is_simple
652 || result_is_bytes_any_lang;
653 let result_is_array = call_config.result_is_array;
654 let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);
659
660 let method_name = fixture.id.to_upper_camel_case();
661 let description = &fixture.description;
662 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
663 let is_async = call_config.r#async;
664
665 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
667 let collect_snippet_opt = if is_streaming && !expects_error {
668 crate::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(lang, result_var, "chunks")
669 } else {
670 None
671 };
672 if is_streaming && !expects_error && collect_snippet_opt.is_none() {
679 if is_async {
680 let _ = writeln!(out, " func test{method_name}() async throws {{");
681 } else {
682 let _ = writeln!(out, " func test{method_name}() throws {{");
683 }
684 let _ = writeln!(out, " // {description}");
685 let _ = writeln!(
686 out,
687 " try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
688 fixture.id
689 );
690 let _ = writeln!(out, " }}");
691 return;
692 }
693 let collect_snippet = collect_snippet_opt.unwrap_or_default();
694
695 let has_unresolvable_json_object_arg = {
702 let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
703 options_via.is_none() && args.iter().any(|a| a.arg_type == "json_object" && a.name != "config")
704 };
705
706 if has_unresolvable_json_object_arg {
707 if is_async {
708 let _ = writeln!(out, " func test{method_name}() async throws {{");
709 } else {
710 let _ = writeln!(out, " func test{method_name}() throws {{");
711 }
712 let _ = writeln!(out, " // {description}");
713 let _ = writeln!(
714 out,
715 " try XCTSkipIf(true, \"swift: json_object request construction requires options_via configuration (fixture: {})\");",
716 fixture.id
717 );
718 let _ = writeln!(out, " }}");
719 return;
720 }
721
722 let mut visitor_setup_lines: Vec<String> = Vec::new();
726 let visitor_handle_expr: Option<String> = fixture
727 .visitor
728 .as_ref()
729 .map(|spec| super::swift_visitors::build_swift_visitor(&mut visitor_setup_lines, spec, &fixture.id));
730
731 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
735
736 let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
741 let per_call = call_overrides.map(|o| &o.enum_fields);
742 if let Some(pc) = per_call {
743 if !pc.is_empty() {
744 let mut merged = enum_fields.clone();
745 merged.extend(pc.keys().cloned());
746 std::borrow::Cow::Owned(merged)
747 } else {
748 std::borrow::Cow::Borrowed(enum_fields)
749 }
750 } else {
751 std::borrow::Cow::Borrowed(enum_fields)
752 }
753 };
754
755 let options_via_str: Option<&str> = call_overrides.and_then(|o| o.options_via.as_deref());
756 let options_type_str: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
757 let handle_config_fn_owned: Option<String> = call_config
761 .overrides
762 .get("c")
763 .and_then(|c| c.c_engine_factory.as_deref())
764 .map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
765 let (mut setup_lines, args_str) = build_args_and_setup(
766 &fixture.input,
767 args,
768 &fixture.id,
769 fixture.has_host_root_route(),
770 &function_name,
771 options_via_str,
772 options_type_str,
773 handle_config_fn_owned.as_deref(),
774 visitor_handle_expr.as_deref(),
775 );
776 if !visitor_setup_lines.is_empty() {
778 visitor_setup_lines.extend(setup_lines);
779 setup_lines = visitor_setup_lines;
780 }
781
782 let args_str = if extra_args.is_empty() {
784 args_str
785 } else if args_str.is_empty() {
786 extra_args.join(", ")
787 } else {
788 format!("{args_str}, {}", extra_args.join(", "))
789 };
790
791 let has_mock = fixture.mock_response.is_some();
796 let (call_setup, call_expr) = if let Some(_factory) = client_factory {
797 let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
798 let mock_url = if fixture.has_host_root_route() {
799 format!(
800 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\")",
801 fixture.id
802 )
803 } else {
804 format!(
805 "ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\"",
806 fixture.id
807 )
808 };
809 let client_constructor = if has_mock {
810 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
811 } else {
812 if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
814 format!(
815 "let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n \
816 let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n \
817 let _client = try DefaultClient(apiKey: _apiKey ?? \"test-key\", baseUrl: _baseUrl)"
818 )
819 } else {
820 format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
821 }
822 };
823 let expr = if is_async {
824 format!("try await _client.{function_name}({args_str})")
825 } else {
826 format!("try _client.{function_name}({args_str})")
827 };
828 (Some(client_constructor), expr)
829 } else {
830 let expr = if is_async {
832 format!("try await {function_name}({args_str})")
833 } else {
834 format!("try {function_name}({args_str})")
835 };
836 (None, expr)
837 };
838 let _ = function_name;
840
841 if is_async {
842 let _ = writeln!(out, " func test{method_name}() async throws {{");
843 } else {
844 let _ = writeln!(out, " func test{method_name}() throws {{");
845 }
846 let _ = writeln!(out, " // {description}");
847
848 if expects_error {
849 if is_async {
853 let _ = writeln!(out, " do {{");
858 for line in &setup_lines {
859 let _ = writeln!(out, " {line}");
860 }
861 if let Some(setup) = &call_setup {
862 let _ = writeln!(out, " {setup}");
863 }
864 let _ = writeln!(out, " _ = {call_expr}");
865 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
866 let _ = writeln!(out, " }} catch {{");
867 let _ = writeln!(out, " // success");
868 let _ = writeln!(out, " }}");
869 } else {
870 let _ = writeln!(out, " do {{");
877 for line in &setup_lines {
878 let _ = writeln!(out, " {line}");
879 }
880 if let Some(setup) = &call_setup {
881 let _ = writeln!(out, " {setup}");
882 }
883 let _ = writeln!(out, " _ = {call_expr}");
884 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
885 let _ = writeln!(out, " }} catch {{");
886 let _ = writeln!(out, " // success");
887 let _ = writeln!(out, " }}");
888 }
889 let _ = writeln!(out, " }}");
890 return;
891 }
892
893 for line in &setup_lines {
894 let _ = writeln!(out, " {line}");
895 }
896
897 if let Some(setup) = &call_setup {
899 let _ = writeln!(out, " {setup}");
900 }
901
902 let _ = writeln!(out, " let {result_var} = {call_expr}");
903
904 if !collect_snippet.is_empty() {
907 for line in collect_snippet.lines() {
908 let _ = writeln!(out, " {line}");
909 }
910 }
911
912 let fixture_root_type: Option<String> = swift_call_result_type(call_config);
917 let fixture_resolver = field_resolver.with_swift_root_type(fixture_root_type);
918
919 for assertion in &fixture.assertions {
920 render_assertion(
921 out,
922 assertion,
923 result_var,
924 &fixture_resolver,
925 result_is_simple,
926 result_is_array,
927 result_is_option,
928 &effective_enum_fields,
929 is_streaming,
930 );
931 }
932
933 let _ = writeln!(out, " }}");
934}
935
936#[allow(clippy::too_many_arguments)]
937fn build_args_and_setup(
951 input: &serde_json::Value,
952 args: &[crate::config::ArgMapping],
953 fixture_id: &str,
954 has_host_root_route: bool,
955 function_name: &str,
956 options_via: Option<&str>,
957 options_type: Option<&str>,
958 handle_config_fn: Option<&str>,
959 visitor_handle_expr: Option<&str>,
960) -> (Vec<String>, String) {
961 if args.is_empty() {
962 return (Vec::new(), String::new());
963 }
964
965 let mut setup_lines: Vec<String> = Vec::new();
966 let mut parts: Vec<String> = Vec::new();
967
968 let later_emits: Vec<bool> = (0..args.len())
973 .map(|i| {
974 args.iter().skip(i + 1).any(|a| {
975 let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
976 let v = input.get(f);
977 let has_value = matches!(v, Some(x) if !x.is_null());
978 has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
979 })
980 })
981 .collect();
982
983 for (idx, arg) in args.iter().enumerate() {
984 if arg.arg_type == "mock_url" {
985 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
986 let url_expr = if has_host_root_route {
987 format!(
988 "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\")"
989 )
990 } else {
991 format!("ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"")
992 };
993 setup_lines.push(format!("let {} = {url_expr}", arg.name));
994 parts.push(arg.name.clone());
995 continue;
996 }
997
998 if arg.arg_type == "handle" {
999 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1000 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1001 let config_val = input.get(field);
1002 let has_config = config_val
1003 .is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
1004 if has_config {
1005 if let Some(from_json_fn) = handle_config_fn {
1006 let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
1007 let escaped = escape_swift_str(&json_str);
1008 let config_var = format!("{}Config", arg.name.to_lower_camel_case());
1009 setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
1010 setup_lines.push(format!("let {var_name} = try createEngine({config_var})"));
1011 } else {
1012 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
1013 }
1014 } else {
1015 setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
1016 }
1017 parts.push(var_name);
1018 continue;
1019 }
1020
1021 if arg.arg_type == "bytes" {
1026 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1027 let val = input.get(field);
1028 match val {
1029 None | Some(serde_json::Value::Null) if arg.optional => {
1030 if later_emits[idx] {
1031 parts.push("nil".to_string());
1032 }
1033 }
1034 None | Some(serde_json::Value::Null) => {
1035 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1036 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1037 parts.push(var_name);
1038 }
1039 Some(serde_json::Value::String(s)) => {
1040 let escaped = escape_swift(s);
1041 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1042 let data_var = format!("{}Data", arg.name.to_lower_camel_case());
1043 setup_lines.push(format!(
1044 "let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
1045 ));
1046 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1047 setup_lines.push(format!("for _byte in {data_var} {{ {var_name}.push(value: _byte) }}"));
1048 parts.push(var_name);
1049 }
1050 Some(serde_json::Value::Array(arr)) => {
1051 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1052 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1053 for v in arr {
1054 if let Some(n) = v.as_u64() {
1055 setup_lines.push(format!("{var_name}.push(value: UInt8({n}))"));
1056 }
1057 }
1058 parts.push(var_name);
1059 }
1060 Some(other) => {
1061 let json_str = serde_json::to_string(other).unwrap_or_default();
1063 let escaped = escape_swift(&json_str);
1064 let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1065 setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1066 setup_lines.push(format!(
1067 "for _byte in Array(\"{escaped}\".utf8) {{ {var_name}.push(value: _byte) }}"
1068 ));
1069 parts.push(var_name);
1070 }
1071 }
1072 continue;
1073 }
1074
1075 let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
1081 let is_batch_fn = function_name.starts_with("batch") || function_name.starts_with("Batch");
1082 if is_config_arg && !is_batch_fn {
1083 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1084 let val = input.get(field);
1085 let json_str = match val {
1086 None | Some(serde_json::Value::Null) => "{}".to_string(),
1087 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1088 };
1089 let escaped = escape_swift(&json_str);
1090 let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1091 let from_json_fn = if let Some(type_name) = options_type {
1093 format!("{}FromJson", type_name.to_lower_camel_case())
1094 } else {
1095 "extractionConfigFromJson".to_string()
1096 };
1097 setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1098 parts.push(var_name);
1099 continue;
1100 }
1101
1102 if arg.arg_type == "json_object" && options_via == Some("from_json") {
1109 if let Some(type_name) = options_type {
1110 let resolved_val = super::resolve_field(input, &arg.field);
1111 let json_str = match resolved_val {
1112 serde_json::Value::Null => "{}".to_string(),
1113 v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1114 };
1115 let escaped = escape_swift(&json_str);
1116 let var_name = format!("_{}", arg.name.to_lower_camel_case());
1117 if let Some(handle_expr) = visitor_handle_expr {
1118 let with_visitor_fn = format!("{}FromJsonWithVisitor", type_name.to_lower_camel_case());
1123 let handle_var = format!("_visitorHandle_{}", var_name.trim_start_matches('_'));
1124 setup_lines.push(format!("let {handle_var} = {handle_expr}"));
1125 setup_lines.push(format!(
1126 "let {var_name} = try {with_visitor_fn}(\"{escaped}\", {handle_var})"
1127 ));
1128 } else {
1129 let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
1130 setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1131 }
1132 parts.push(var_name);
1133 continue;
1134 }
1135 }
1136
1137 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1138 let val = input.get(field);
1139 match val {
1140 None | Some(serde_json::Value::Null) if arg.optional => {
1141 if later_emits[idx] {
1145 parts.push("nil".to_string());
1146 }
1147 }
1148 None | Some(serde_json::Value::Null) => {
1149 let default_val = match arg.arg_type.as_str() {
1150 "string" => "\"\"".to_string(),
1151 "int" | "integer" => "0".to_string(),
1152 "float" | "number" => "0.0".to_string(),
1153 "bool" | "boolean" => "false".to_string(),
1154 _ => "nil".to_string(),
1155 };
1156 parts.push(default_val);
1157 }
1158 Some(v) => {
1159 parts.push(json_to_swift(v));
1160 }
1161 }
1162 }
1163
1164 (setup_lines, parts.join(", "))
1165}
1166
1167#[allow(clippy::too_many_arguments)]
1168fn render_assertion(
1169 out: &mut String,
1170 assertion: &Assertion,
1171 result_var: &str,
1172 field_resolver: &FieldResolver,
1173 result_is_simple: bool,
1174 result_is_array: bool,
1175 result_is_option: bool,
1176 enum_fields: &HashSet<String>,
1177 is_streaming: bool,
1178) {
1179 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1184 if let Some(f) = &assertion.field {
1189 let is_streaming_usage_path =
1190 is_streaming && (f == "usage" || (f.starts_with("usage.") || f.starts_with("usage[")));
1191 if !f.is_empty()
1192 && (crate::codegen::streaming_assertions::is_streaming_virtual_field(f) || is_streaming_usage_path)
1193 {
1194 if let Some(expr) =
1195 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "swift", "chunks")
1196 {
1197 let line = match assertion.assertion_type.as_str() {
1198 "count_min" => {
1199 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1200 format!(" XCTAssertGreaterThanOrEqual(chunks.count, {n})\n")
1201 } else {
1202 String::new()
1203 }
1204 }
1205 "count_equals" => {
1206 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1207 format!(" XCTAssertEqual(chunks.count, {n})\n")
1208 } else {
1209 String::new()
1210 }
1211 }
1212 "equals" => {
1213 if let Some(serde_json::Value::String(s)) = &assertion.value {
1214 let escaped = escape_swift(s);
1215 format!(" XCTAssertEqual({expr}, \"{escaped}\")\n")
1216 } else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
1217 format!(" XCTAssertEqual({expr}, {b})\n")
1218 } else {
1219 String::new()
1220 }
1221 }
1222 "not_empty" => {
1223 format!(" XCTAssertFalse({expr}.isEmpty, \"expected non-empty\")\n")
1224 }
1225 "is_empty" => {
1226 format!(" XCTAssertTrue({expr}.isEmpty, \"expected empty\")\n")
1227 }
1228 "is_true" => {
1229 format!(" XCTAssertTrue({expr})\n")
1230 }
1231 "is_false" => {
1232 format!(" XCTAssertFalse({expr})\n")
1233 }
1234 "greater_than" => {
1235 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1236 format!(" XCTAssertGreaterThan(chunks.count, {n})\n")
1237 } else {
1238 String::new()
1239 }
1240 }
1241 "contains" => {
1242 if let Some(serde_json::Value::String(s)) = &assertion.value {
1243 let escaped = escape_swift(s);
1244 format!(
1245 " XCTAssertTrue({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\")\n"
1246 )
1247 } else {
1248 String::new()
1249 }
1250 }
1251 _ => format!(
1252 " // streaming field '{f}': assertion type '{}' not rendered\n",
1253 assertion.assertion_type
1254 ),
1255 };
1256 if !line.is_empty() {
1257 out.push_str(&line);
1258 }
1259 }
1260 return;
1261 }
1262 }
1263
1264 if let Some(f) = &assertion.field {
1266 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1267 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1268 return;
1269 }
1270 }
1271
1272 if let Some(f) = &assertion.field {
1277 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1278 let _ = writeln!(
1279 out,
1280 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Swift)"
1281 );
1282 return;
1283 }
1284 }
1285
1286 let field_is_enum = assertion
1288 .field
1289 .as_deref()
1290 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1291
1292 let field_is_optional = assertion.field.as_deref().is_some_and(|f| {
1293 !f.is_empty() && (field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f)))
1294 });
1295 let field_is_array = assertion.field.as_deref().is_some_and(|f| {
1296 !f.is_empty()
1297 && (field_resolver.is_array(f)
1298 || field_resolver.is_array(field_resolver.resolve(f))
1299 || field_resolver.is_collection_root(f)
1300 || field_resolver.is_collection_root(field_resolver.resolve(f)))
1301 });
1302
1303 let field_expr_raw = if result_is_simple {
1304 result_var.to_string()
1305 } else {
1306 match &assertion.field {
1307 Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
1308 _ => result_var.to_string(),
1309 }
1310 };
1311
1312 let local_suffix = {
1322 use std::hash::{Hash, Hasher};
1323 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1324 assertion.field.hash(&mut hasher);
1325 assertion
1326 .value
1327 .as_ref()
1328 .map(|v| v.to_string())
1329 .unwrap_or_default()
1330 .hash(&mut hasher);
1331 format!(
1332 "{}_{:x}",
1333 assertion.assertion_type.replace(['-', '.'], "_"),
1334 hasher.finish() & 0xffff_ffff,
1335 )
1336 };
1337 let (vec_setup, field_expr, is_map_subscript) = materialise_vec_temporaries(&field_expr_raw, &local_suffix);
1338 let field_uses_traversal = assertion.field.as_deref().is_some_and(|f| f.contains("[]."));
1343 let traversal_skips_field_expr = field_uses_traversal
1344 && matches!(
1345 assertion.assertion_type.as_str(),
1346 "contains" | "not_contains" | "not_empty" | "is_empty"
1347 );
1348 if !traversal_skips_field_expr {
1349 for line in &vec_setup {
1350 let _ = writeln!(out, " {line}");
1351 }
1352 }
1353
1354 let accessor_is_optional = field_expr.contains("?.");
1360
1361 let string_expr = if is_map_subscript {
1370 format!("({field_expr} ?? \"\")")
1374 } else if field_is_enum && (field_is_optional || accessor_is_optional) {
1375 format!("({field_expr}?.toString() ?? \"\")")
1378 } else if field_is_enum {
1379 format!("{field_expr}.toString()")
1384 } else if field_is_optional {
1385 format!("({field_expr}?.toString() ?? \"\")")
1387 } else if accessor_is_optional {
1388 format!("({field_expr}.toString() ?? \"\")")
1391 } else {
1392 format!("{field_expr}.toString()")
1393 };
1394
1395 match assertion.assertion_type.as_str() {
1396 "equals" => {
1397 if let Some(expected) = &assertion.value {
1398 let swift_val = json_to_swift(expected);
1399 if expected.is_string() {
1400 if field_is_enum {
1401 let trim_expr =
1405 format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1406 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1407 } else {
1408 let trim_expr =
1413 format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1414 let _ = writeln!(out, " XCTAssertEqual({trim_expr}, {swift_val})");
1415 }
1416 } else {
1417 let _ = writeln!(out, " XCTAssertEqual({field_expr}, {swift_val})");
1418 }
1419 }
1420 }
1421 "contains" => {
1422 if let Some(expected) = &assertion.value {
1423 let swift_val = json_to_swift(expected);
1424 let no_field = assertion.field.as_deref().is_none_or(|f| f.is_empty());
1427 if result_is_simple && result_is_array && no_field {
1428 let _ = writeln!(
1431 out,
1432 " XCTAssertTrue({result_var}.map {{ $0.as_str().toString() }}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1433 );
1434 } else {
1435 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1437 if let Some(dot) = f.find("[].") {
1438 let array_part = &f[..dot];
1439 let elem_part = &f[dot + 3..];
1440 let line = swift_traversal_contains_assert(
1441 array_part,
1442 elem_part,
1443 f,
1444 &swift_val,
1445 result_var,
1446 false,
1447 &format!("expected to contain: \\({swift_val})"),
1448 enum_fields,
1449 field_resolver,
1450 );
1451 let _ = writeln!(out, "{line}");
1452 true
1453 } else {
1454 false
1455 }
1456 } else {
1457 false
1458 };
1459 if !traversal_handled {
1460 let field_is_array = assertion
1462 .field
1463 .as_deref()
1464 .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
1465 if field_is_array {
1466 let contains_expr =
1467 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1468 let _ = writeln!(
1469 out,
1470 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1471 );
1472 } else if field_is_enum {
1473 let _ = writeln!(
1476 out,
1477 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1478 );
1479 } else {
1480 let _ = writeln!(
1481 out,
1482 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1483 );
1484 }
1485 }
1486 }
1487 }
1488 }
1489 "contains_all" => {
1490 if let Some(values) = &assertion.values {
1491 if let Some(f) = assertion.field.as_deref() {
1493 if let Some(dot) = f.find("[].") {
1494 let array_part = &f[..dot];
1495 let elem_part = &f[dot + 3..];
1496 for val in values {
1497 let swift_val = json_to_swift(val);
1498 let line = swift_traversal_contains_assert(
1499 array_part,
1500 elem_part,
1501 f,
1502 &swift_val,
1503 result_var,
1504 false,
1505 &format!("expected to contain: \\({swift_val})"),
1506 enum_fields,
1507 field_resolver,
1508 );
1509 let _ = writeln!(out, "{line}");
1510 }
1511 } else {
1513 let field_is_array = field_resolver.is_array(field_resolver.resolve(f));
1515 if field_is_array {
1516 let contains_expr =
1517 swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1518 for val in values {
1519 let swift_val = json_to_swift(val);
1520 let _ = writeln!(
1521 out,
1522 " XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1523 );
1524 }
1525 } else if field_is_enum {
1526 for val in values {
1529 let swift_val = json_to_swift(val);
1530 let _ = writeln!(
1531 out,
1532 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1533 );
1534 }
1535 } else {
1536 for val in values {
1537 let swift_val = json_to_swift(val);
1538 let _ = writeln!(
1539 out,
1540 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1541 );
1542 }
1543 }
1544 }
1545 } else {
1546 for val in values {
1548 let swift_val = json_to_swift(val);
1549 let _ = writeln!(
1550 out,
1551 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1552 );
1553 }
1554 }
1555 }
1556 }
1557 "not_contains" => {
1558 if let Some(expected) = &assertion.value {
1559 let swift_val = json_to_swift(expected);
1560 let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1562 if let Some(dot) = f.find("[].") {
1563 let array_part = &f[..dot];
1564 let elem_part = &f[dot + 3..];
1565 let line = swift_traversal_contains_assert(
1566 array_part,
1567 elem_part,
1568 f,
1569 &swift_val,
1570 result_var,
1571 true,
1572 &format!("expected NOT to contain: \\({swift_val})"),
1573 enum_fields,
1574 field_resolver,
1575 );
1576 let _ = writeln!(out, "{line}");
1577 true
1578 } else {
1579 false
1580 }
1581 } else {
1582 false
1583 };
1584 if !traversal_handled {
1585 let _ = writeln!(
1586 out,
1587 " XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
1588 );
1589 }
1590 }
1591 }
1592 "not_empty" => {
1593 let traversal_not_empty_handled = if let Some(f) = assertion.field.as_deref() {
1600 if let Some(dot) = f.find("[].") {
1601 let array_part = &f[..dot];
1602 let elem_part = &f[dot + 3..];
1603 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1604 let resolved_full = field_resolver.resolve(f);
1605 let resolved_elem_part = resolved_full
1606 .find("[].")
1607 .map(|d| &resolved_full[d + 3..])
1608 .unwrap_or(elem_part);
1609 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1610 let elem_is_enum = enum_fields.contains(f) || enum_fields.contains(resolved_full);
1611 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1612 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1613 let elem_str = if elem_is_enum {
1614 format!("{elem_accessor}.to_string().toString()")
1615 } else if elem_is_optional {
1616 format!("({elem_accessor}?.toString() ?? \"\")")
1617 } else {
1618 format!("{elem_accessor}.toString()")
1619 };
1620 let _ = writeln!(
1621 out,
1622 " XCTAssertTrue({array_accessor}.contains(where: {{ !{elem_str}.isEmpty }}), \"expected non-empty value\")"
1623 );
1624 true
1625 } else {
1626 false
1627 }
1628 } else {
1629 false
1630 };
1631 if !traversal_not_empty_handled {
1632 if bare_result_is_option {
1633 let _ = writeln!(out, " XCTAssertNotNil({result_var}, \"expected non-nil value\")");
1634 } else if field_is_optional {
1635 let _ = writeln!(out, " XCTAssertNotNil({field_expr}, \"expected non-nil value\")");
1636 } else if field_is_array {
1637 let _ = writeln!(
1638 out,
1639 " XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
1640 );
1641 } else if result_is_simple {
1642 let _ = writeln!(
1644 out,
1645 " XCTAssertFalse({result_var}.isEmpty, \"expected non-empty value\")"
1646 );
1647 } else {
1648 let count_target = swift_count_target(&field_expr, field_resolver, assertion.field.as_deref());
1663 let len_expr = if accessor_is_optional {
1664 format!("({count_target}.count ?? 0)")
1665 } else {
1666 format!("{count_target}.count")
1667 };
1668 let _ = writeln!(
1669 out,
1670 " XCTAssertGreaterThan({len_expr}, 0, \"expected non-empty value\")"
1671 );
1672 }
1673 }
1674 }
1675 "is_empty" => {
1676 if bare_result_is_option {
1677 let _ = writeln!(out, " XCTAssertNil({result_var}, \"expected nil value\")");
1678 } else if field_is_optional {
1679 let _ = writeln!(out, " XCTAssertNil({field_expr}, \"expected nil value\")");
1680 } else if field_is_array {
1681 let _ = writeln!(
1682 out,
1683 " XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
1684 );
1685 } else {
1686 let count_target = swift_count_target(&field_expr, field_resolver, assertion.field.as_deref());
1690 let len_expr = if accessor_is_optional {
1691 format!("({count_target}.count ?? 0)")
1692 } else {
1693 format!("{count_target}.count")
1694 };
1695 let _ = writeln!(out, " XCTAssertEqual({len_expr}, 0, \"expected empty value\")");
1696 }
1697 }
1698 "contains_any" => {
1699 if let Some(values) = &assertion.values {
1700 let checks: Vec<String> = values
1701 .iter()
1702 .map(|v| {
1703 let swift_val = json_to_swift(v);
1704 format!("{string_expr}.contains({swift_val})")
1705 })
1706 .collect();
1707 let joined = checks.join(" || ");
1708 let _ = writeln!(
1709 out,
1710 " XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
1711 );
1712 }
1713 }
1714 "greater_than" => {
1715 if let Some(val) = &assertion.value {
1716 let swift_val = json_to_swift(val);
1717 let field_is_optional = accessor_is_optional
1720 || assertion.field.as_deref().is_some_and(|f| {
1721 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1722 });
1723 let compare_expr = if field_is_optional {
1724 format!("({field_expr} ?? 0)")
1725 } else {
1726 field_expr.clone()
1727 };
1728 let _ = writeln!(out, " XCTAssertGreaterThan({compare_expr}, {swift_val})");
1729 }
1730 }
1731 "less_than" => {
1732 if let Some(val) = &assertion.value {
1733 let swift_val = json_to_swift(val);
1734 let field_is_optional = accessor_is_optional
1735 || assertion.field.as_deref().is_some_and(|f| {
1736 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1737 });
1738 let compare_expr = if field_is_optional {
1739 format!("({field_expr} ?? 0)")
1740 } else {
1741 field_expr.clone()
1742 };
1743 let _ = writeln!(out, " XCTAssertLessThan({compare_expr}, {swift_val})");
1744 }
1745 }
1746 "greater_than_or_equal" => {
1747 if let Some(val) = &assertion.value {
1748 let swift_val = json_to_swift(val);
1749 let field_is_optional = accessor_is_optional
1752 || assertion.field.as_deref().is_some_and(|f| {
1753 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1754 });
1755 let compare_expr = if field_is_optional {
1756 format!("({field_expr} ?? 0)")
1757 } else {
1758 field_expr.clone()
1759 };
1760 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({compare_expr}, {swift_val})");
1761 }
1762 }
1763 "less_than_or_equal" => {
1764 if let Some(val) = &assertion.value {
1765 let swift_val = json_to_swift(val);
1766 let field_is_optional = accessor_is_optional
1767 || assertion.field.as_deref().is_some_and(|f| {
1768 field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1769 });
1770 let compare_expr = if field_is_optional {
1771 format!("({field_expr} ?? 0)")
1772 } else {
1773 field_expr.clone()
1774 };
1775 let _ = writeln!(out, " XCTAssertLessThanOrEqual({compare_expr}, {swift_val})");
1776 }
1777 }
1778 "starts_with" => {
1779 if let Some(expected) = &assertion.value {
1780 let swift_val = json_to_swift(expected);
1781 let _ = writeln!(
1782 out,
1783 " XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
1784 );
1785 }
1786 }
1787 "ends_with" => {
1788 if let Some(expected) = &assertion.value {
1789 let swift_val = json_to_swift(expected);
1790 let _ = writeln!(
1791 out,
1792 " XCTAssertTrue({string_expr}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
1793 );
1794 }
1795 }
1796 "min_length" => {
1797 if let Some(val) = &assertion.value {
1798 if let Some(n) = val.as_u64() {
1799 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({string_expr}.count, {n})");
1802 }
1803 }
1804 }
1805 "max_length" => {
1806 if let Some(val) = &assertion.value {
1807 if let Some(n) = val.as_u64() {
1808 let _ = writeln!(out, " XCTAssertLessThanOrEqual({string_expr}.count, {n})");
1809 }
1810 }
1811 }
1812 "count_min" => {
1813 if let Some(val) = &assertion.value {
1814 if let Some(n) = val.as_u64() {
1815 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1819 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({count_expr}, {n})");
1820 }
1821 }
1822 }
1823 "count_equals" => {
1824 if let Some(val) = &assertion.value {
1825 if let Some(n) = val.as_u64() {
1826 let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1827 let _ = writeln!(out, " XCTAssertEqual({count_expr}, {n})");
1828 }
1829 }
1830 }
1831 "is_true" => {
1832 let _ = writeln!(out, " XCTAssertTrue({field_expr})");
1833 }
1834 "is_false" => {
1835 let _ = writeln!(out, " XCTAssertFalse({field_expr})");
1836 }
1837 "matches_regex" => {
1838 if let Some(expected) = &assertion.value {
1839 let swift_val = json_to_swift(expected);
1840 let _ = writeln!(
1841 out,
1842 " XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
1843 );
1844 }
1845 }
1846 "not_error" => {
1847 }
1849 "error" => {
1850 }
1852 "method_result" => {
1853 let _ = writeln!(out, " // method_result assertions not yet implemented for Swift");
1854 }
1855 other => {
1856 panic!("Swift e2e generator: unsupported assertion type: {other}");
1857 }
1858 }
1859}
1860
1861fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String, bool) {
1886 let Some(idx) = expr.find("()[") else {
1887 return (Vec::new(), expr.to_string(), false);
1888 };
1889 let after_open = idx + 3; let Some(close_rel) = expr[after_open..].find(']') else {
1891 return (Vec::new(), expr.to_string(), false);
1892 };
1893 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);
1898 let method = &expr[method_dot + 1..idx];
1899 let local = format!("_vec_{}_{}", method, name_suffix);
1900
1901 let inner = subscript.trim_start_matches('[').trim_end_matches(']');
1906 let is_string_key = inner.starts_with('"') && inner.ends_with('"');
1907 let setup = if is_string_key {
1908 format!(
1909 "let {local} = (try? JSONSerialization.jsonObject(with: ({prefix}.toString() ?? \"{{}}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
1910 )
1911 } else {
1912 format!("let {local} = {prefix}")
1913 };
1914
1915 let rewritten = format!("{local}{subscript}{tail}");
1916 (vec![setup], rewritten, is_string_key)
1917}
1918
1919fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
1922 let resolved = field_resolver.resolve(field);
1923 let parts: Vec<&str> = resolved.split('.').collect();
1924
1925 let mut out = result_var.to_string();
1928 let mut has_optional = false;
1929 let mut path_so_far = String::new();
1930 let total = parts.len();
1931 for (i, part) in parts.iter().enumerate() {
1932 let is_leaf = i == total - 1;
1933 let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
1937 (&part[..bracket_pos], Some(&part[bracket_pos..]))
1938 } else {
1939 (part, None)
1940 };
1941
1942 if !path_so_far.is_empty() {
1943 path_so_far.push('.');
1944 }
1945 let base_path = {
1949 let mut p = path_so_far.clone();
1950 p.push_str(field_name);
1951 p
1952 };
1953 path_so_far.push_str(part);
1956
1957 out.push('.');
1958 out.push_str(field_name);
1959 if let Some(sub) = subscript {
1960 let field_is_optional = field_resolver.is_optional(&base_path);
1964 if field_is_optional {
1965 out.push_str("()?");
1966 has_optional = true;
1967 } else {
1968 out.push_str("()");
1969 }
1970 out.push_str(sub);
1971 } else {
1981 out.push_str("()");
1982 if !is_leaf && field_resolver.is_optional(&base_path) {
1985 out.push('?');
1986 has_optional = true;
1987 }
1988 }
1989 }
1990 (out, has_optional)
1991}
1992
1993#[allow(clippy::too_many_arguments)]
2015fn swift_traversal_contains_assert(
2016 array_part: &str,
2017 element_part: &str,
2018 full_field: &str,
2019 val_expr: &str,
2020 result_var: &str,
2021 negate: bool,
2022 msg: &str,
2023 enum_fields: &std::collections::HashSet<String>,
2024 field_resolver: &FieldResolver,
2025) -> String {
2026 let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
2027 let resolved_full = field_resolver.resolve(full_field);
2028 let resolved_elem_part = resolved_full
2029 .find("[].")
2030 .map(|d| &resolved_full[d + 3..])
2031 .unwrap_or(element_part);
2032 let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
2033 let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
2034 let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
2035 || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
2036 let elem_str = if elem_is_enum {
2037 format!("{elem_accessor}.toString()")
2040 } else if elem_is_optional {
2041 format!("({elem_accessor}?.toString() ?? \"\")")
2042 } else {
2043 format!("{elem_accessor}.toString()")
2044 };
2045 let assert_fn = if negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
2046 format!(" {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
2047}
2048
2049fn swift_array_contains_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
2050 let Some(f) = field else {
2051 return format!("{result_var}.map {{ $0.as_str().toString() }}");
2052 };
2053 let (accessor, _has_optional) = swift_build_accessor(f, result_var, field_resolver);
2054 format!("{accessor}?.map {{ $0.as_str().toString() }}")
2057}
2058
2059fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
2068 let Some(f) = field else {
2069 return format!("{result_var}.count");
2070 };
2071 let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
2072 if field_resolver.is_optional(f) {
2074 has_optional = true;
2075 }
2076 if has_optional {
2077 if accessor.contains("?.") {
2080 format!("{accessor}.count ?? 0")
2081 } else {
2082 format!("({accessor}?.count ?? 0)")
2085 }
2086 } else {
2087 format!("{accessor}.count")
2088 }
2089}
2090
2091fn json_to_swift(value: &serde_json::Value) -> String {
2093 match value {
2094 serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
2095 serde_json::Value::Bool(b) => b.to_string(),
2096 serde_json::Value::Number(n) => n.to_string(),
2097 serde_json::Value::Null => "nil".to_string(),
2098 serde_json::Value::Array(arr) => {
2099 let items: Vec<String> = arr.iter().map(json_to_swift).collect();
2100 format!("[{}]", items.join(", "))
2101 }
2102 serde_json::Value::Object(_) => {
2103 let json_str = serde_json::to_string(value).unwrap_or_default();
2104 format!("\"{}\"", escape_swift(&json_str))
2105 }
2106 }
2107}
2108
2109fn escape_swift(s: &str) -> String {
2111 escape_swift_str(s)
2112}
2113
2114fn swift_count_target(field_expr: &str, field_resolver: &FieldResolver, field: Option<&str>) -> String {
2131 let is_method_call = field_expr.trim_end().ends_with(')');
2132 if !is_method_call {
2133 return field_expr.to_string();
2134 }
2135 if let Some(f) = field
2136 && field_resolver.leaf_is_vec_via_swift_map(field_resolver.resolve(f))
2137 {
2138 return field_expr.to_string();
2139 }
2140 format!("{field_expr}.toString()")
2141}
2142
2143fn swift_call_result_type(call_config: &alef_core::config::e2e::CallConfig) -> Option<String> {
2157 const LOOKUP_LANGS: &[&str] = &["c", "csharp", "java", "kotlin", "go", "php"];
2158 for lang in LOOKUP_LANGS {
2159 if let Some(o) = call_config.overrides.get(*lang)
2160 && let Some(rt) = o.result_type.as_deref()
2161 && !rt.is_empty()
2162 {
2163 return Some(rt.to_string());
2164 }
2165 }
2166 None
2167}
2168
2169fn swift_first_class_field_supported(ty: &alef_core::ir::TypeRef) -> bool {
2174 use alef_core::ir::TypeRef;
2175 match ty {
2176 TypeRef::Primitive(_) | TypeRef::String => true,
2177 TypeRef::Optional(inner) => swift_first_class_field_supported(inner),
2178 _ => false,
2179 }
2180}
2181
2182fn build_swift_first_class_map(
2195 type_defs: &[alef_core::ir::TypeDef],
2196 e2e_config: &crate::config::E2eConfig,
2197) -> SwiftFirstClassMap {
2198 use alef_core::ir::TypeRef;
2199 let mut first_class_types: HashSet<String> = HashSet::new();
2200 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2201 let mut vec_field_names: HashSet<String> = HashSet::new();
2202 fn inner_named(ty: &TypeRef) -> Option<String> {
2203 match ty {
2204 TypeRef::Named(n) => Some(n.clone()),
2205 TypeRef::Optional(inner) | TypeRef::Vec(inner) => inner_named(inner),
2206 _ => None,
2207 }
2208 }
2209 fn is_vec_ty(ty: &TypeRef) -> bool {
2210 match ty {
2211 TypeRef::Vec(_) => true,
2212 TypeRef::Optional(inner) => is_vec_ty(inner),
2213 _ => false,
2214 }
2215 }
2216 for td in type_defs {
2217 let is_first_class = !td.is_opaque
2218 && td.has_serde
2219 && !td.fields.is_empty()
2220 && td.fields.iter().all(|f| swift_first_class_field_supported(&f.ty));
2221 if is_first_class {
2222 first_class_types.insert(td.name.clone());
2223 }
2224 let mut td_field_types: HashMap<String, String> = HashMap::new();
2225 for f in &td.fields {
2226 if let Some(named) = inner_named(&f.ty) {
2227 td_field_types.insert(f.name.clone(), named);
2228 }
2229 if is_vec_ty(&f.ty) {
2230 vec_field_names.insert(f.name.clone());
2231 }
2232 }
2233 if !td_field_types.is_empty() {
2234 field_types.insert(td.name.clone(), td_field_types);
2235 }
2236 }
2237 let root_type = if e2e_config.result_fields.is_empty() {
2241 None
2242 } else {
2243 let matches: Vec<&alef_core::ir::TypeDef> = type_defs
2244 .iter()
2245 .filter(|td| {
2246 let names: HashSet<&str> = td.fields.iter().map(|f| f.name.as_str()).collect();
2247 e2e_config.result_fields.iter().all(|rf| names.contains(rf.as_str()))
2248 })
2249 .collect();
2250 if matches.len() == 1 {
2251 Some(matches[0].name.clone())
2252 } else {
2253 None
2254 }
2255 };
2256 SwiftFirstClassMap {
2257 first_class_types,
2258 field_types,
2259 vec_field_names,
2260 root_type,
2261 }
2262}
2263
2264#[cfg(test)]
2265mod tests {
2266 use super::*;
2267 use crate::field_access::FieldResolver;
2268 use std::collections::{HashMap, HashSet};
2269
2270 fn make_resolver_tool_calls() -> FieldResolver {
2271 let mut optional = HashSet::new();
2275 optional.insert("choices.message.tool_calls".to_string());
2276 let mut arrays = HashSet::new();
2277 arrays.insert("choices".to_string());
2278 FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
2279 }
2280
2281 #[test]
2288 fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
2289 let resolver = make_resolver_tool_calls();
2290 let (accessor, has_optional) =
2293 swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
2294 assert!(
2297 accessor.contains("tool_calls()?[0]"),
2298 "expected `tool_calls()?[0]` for optional tool_calls, got: {accessor}"
2299 );
2300 assert!(
2302 !accessor.contains("?[0]?"),
2303 "must not emit trailing `?` after subscript index: {accessor}"
2304 );
2305 assert!(has_optional, "expected has_optional=true for optional field chain");
2307 assert!(
2309 accessor.contains("[0].function()"),
2310 "expected `.function()` (non-optional) after subscript: {accessor}"
2311 );
2312 }
2313}