1use crate::config::E2eConfig;
8use crate::escape::{escape_java as escape_swift_str, expand_fixture_templates, sanitize_filename, sanitize_ident};
9use crate::field_access::FieldResolver;
10use crate::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
11use alef_core::backend::GeneratedFile;
12use alef_core::config::ResolvedCrateConfig;
13use alef_core::hash::{self, CommentStyle};
14use alef_core::template_versions::toolchain;
15use anyhow::Result;
16use heck::{ToLowerCamelCase, ToUpperCamelCase};
17use std::collections::HashSet;
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22use super::client;
23
24pub struct SwiftE2eCodegen;
26
27impl E2eCodegen for SwiftE2eCodegen {
28 fn generate(
29 &self,
30 groups: &[FixtureGroup],
31 e2e_config: &E2eConfig,
32 config: &ResolvedCrateConfig,
33 ) -> Result<Vec<GeneratedFile>> {
34 let lang = self.language_name();
35 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
36
37 let mut files = Vec::new();
38
39 let call = &e2e_config.call;
41 let overrides = call.overrides.get(lang);
42 let function_name = overrides
43 .and_then(|o| o.function.as_ref())
44 .cloned()
45 .unwrap_or_else(|| call.function.clone());
46 let result_var = &call.result_var;
47 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
48
49 let swift_pkg = e2e_config.resolve_package("swift");
51 let pkg_name = swift_pkg
52 .as_ref()
53 .and_then(|p| p.name.as_ref())
54 .cloned()
55 .unwrap_or_else(|| config.name.to_upper_camel_case());
56 let pkg_path = swift_pkg
57 .as_ref()
58 .and_then(|p| p.path.as_ref())
59 .cloned()
60 .unwrap_or_else(|| "../../packages/swift".to_string());
61 let pkg_version = swift_pkg
62 .as_ref()
63 .and_then(|p| p.version.as_ref())
64 .cloned()
65 .unwrap_or_else(|| "0.1.0".to_string());
66
67 let module_name = pkg_name.as_str();
69
70 let registry_url = config
74 .try_github_repo()
75 .map(|repo| {
76 let base = repo.trim_end_matches('/').trim_end_matches(".git");
77 format!("{base}.git")
78 })
79 .unwrap_or_else(|_| format!("https://example.invalid/{module_name}.git"));
80
81 files.push(GeneratedFile {
84 path: output_base.join("Package.swift"),
85 content: render_package_swift(module_name, ®istry_url, &pkg_path, &pkg_version, e2e_config.dep_mode),
86 generated_header: false,
87 });
88
89 let tests_base = normalize_path(&output_base.join(&pkg_path));
103
104 let field_resolver = FieldResolver::new(
105 &e2e_config.fields,
106 &e2e_config.fields_optional,
107 &e2e_config.result_fields,
108 &e2e_config.fields_array,
109 &HashSet::new(),
110 );
111
112 for group in groups {
114 let active: Vec<&Fixture> = group
115 .fixtures
116 .iter()
117 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
118 .collect();
119
120 if active.is_empty() {
121 continue;
122 }
123
124 let class_name = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
125 let filename = format!("{class_name}.swift");
126 let content = render_test_file(
127 &group.category,
128 &active,
129 e2e_config,
130 module_name,
131 &class_name,
132 &function_name,
133 result_var,
134 &e2e_config.call.args,
135 &field_resolver,
136 result_is_simple,
137 &e2e_config.fields_enum,
138 );
139 files.push(GeneratedFile {
140 path: tests_base
141 .join("Tests")
142 .join(format!("{module_name}Tests"))
143 .join(filename),
144 content,
145 generated_header: true,
146 });
147 }
148
149 Ok(files)
150 }
151
152 fn language_name(&self) -> &'static str {
153 "swift"
154 }
155}
156
157fn render_package_swift(
162 module_name: &str,
163 registry_url: &str,
164 pkg_path: &str,
165 pkg_version: &str,
166 dep_mode: crate::config::DependencyMode,
167) -> String {
168 let min_macos = toolchain::SWIFT_MIN_MACOS;
169
170 let (dep_block, product_dep) = match dep_mode {
174 crate::config::DependencyMode::Registry => {
175 let dep = format!(r#" .package(url: "{registry_url}", from: "{pkg_version}")"#);
176 let pkg_id = registry_url
177 .trim_end_matches('/')
178 .trim_end_matches(".git")
179 .split('/')
180 .next_back()
181 .unwrap_or(module_name);
182 let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
183 (dep, prod)
184 }
185 crate::config::DependencyMode::Local => {
186 let dep = format!(r#" .package(path: "{pkg_path}")"#);
187 let pkg_id = pkg_path
188 .trim_end_matches('/')
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 };
196 let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
199 format!(
200 r#"// swift-tools-version: 6.0
201import PackageDescription
202
203let package = Package(
204 name: "E2eSwift",
205 platforms: [
206 .macOS(.v{min_macos_major}),
207 ],
208 dependencies: [
209{dep_block},
210 ],
211 targets: [
212 .testTarget(
213 name: "{module_name}Tests",
214 dependencies: [{product_dep}]
215 ),
216 ]
217)
218"#
219 )
220}
221
222#[allow(clippy::too_many_arguments)]
223fn render_test_file(
224 category: &str,
225 fixtures: &[&Fixture],
226 e2e_config: &E2eConfig,
227 module_name: &str,
228 class_name: &str,
229 function_name: &str,
230 result_var: &str,
231 args: &[crate::config::ArgMapping],
232 field_resolver: &FieldResolver,
233 result_is_simple: bool,
234 enum_fields: &HashSet<String>,
235) -> String {
236 let mut out = String::new();
237 out.push_str(&hash::header(CommentStyle::DoubleSlash));
238 let _ = writeln!(out, "import XCTest");
239 let _ = writeln!(out, "import {module_name}");
240 let _ = writeln!(out);
241 let _ = writeln!(out, "/// E2e tests for category: {category}.");
242 let _ = writeln!(out, "final class {class_name}: XCTestCase {{");
243
244 for fixture in fixtures {
245 if fixture.is_http_test() {
246 render_http_test_method(&mut out, fixture);
247 } else {
248 render_test_method(
249 &mut out,
250 fixture,
251 e2e_config,
252 function_name,
253 result_var,
254 args,
255 field_resolver,
256 result_is_simple,
257 enum_fields,
258 );
259 }
260 let _ = writeln!(out);
261 }
262
263 let _ = writeln!(out, "}}");
264 out
265}
266
267struct SwiftTestClientRenderer;
274
275impl client::TestClientRenderer for SwiftTestClientRenderer {
276 fn language_name(&self) -> &'static str {
277 "swift"
278 }
279
280 fn sanitize_test_name(&self, id: &str) -> String {
281 sanitize_ident(id).to_upper_camel_case()
283 }
284
285 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
291 let _ = writeln!(out, " /// {description}");
292 let _ = writeln!(out, " func test{fn_name}() throws {{");
293 if let Some(reason) = skip_reason {
294 let escaped = escape_swift(reason);
295 let _ = writeln!(out, " try XCTSkipIf(true, \"{escaped}\")");
296 }
297 }
298
299 fn render_test_close(&self, out: &mut String) {
300 let _ = writeln!(out, " }}");
301 }
302
303 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
310 let method = ctx.method.to_uppercase();
311 let fixture_path = escape_swift(ctx.path);
312
313 let _ = writeln!(
314 out,
315 " let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
316 );
317 let _ = writeln!(
318 out,
319 " var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
320 );
321 let _ = writeln!(out, " _req.httpMethod = \"{method}\"");
322
323 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
325 header_pairs.sort_by_key(|(k, _)| k.as_str());
326 for (k, v) in &header_pairs {
327 let expanded_v = expand_fixture_templates(v);
328 let ek = escape_swift(k);
329 let ev = escape_swift(&expanded_v);
330 let _ = writeln!(out, " _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
331 }
332
333 if let Some(body) = ctx.body {
335 let json_str = serde_json::to_string(body).unwrap_or_default();
336 let escaped_body = escape_swift(&json_str);
337 let _ = writeln!(out, " _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
338 let _ = writeln!(
339 out,
340 " _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
341 );
342 }
343
344 let _ = writeln!(out, " var {}: HTTPURLResponse?", ctx.response_var);
345 let _ = writeln!(out, " var _responseData: Data?");
346 let _ = writeln!(out, " let _sema = DispatchSemaphore(value: 0)");
347 let _ = writeln!(
348 out,
349 " URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
350 );
351 let _ = writeln!(out, " {} = resp as? HTTPURLResponse", ctx.response_var);
352 let _ = writeln!(out, " _responseData = data");
353 let _ = writeln!(out, " _sema.signal()");
354 let _ = writeln!(out, " }}.resume()");
355 let _ = writeln!(out, " _sema.wait()");
356 let _ = writeln!(out, " let _resp = try XCTUnwrap({})", ctx.response_var);
357 }
358
359 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
360 let _ = writeln!(out, " XCTAssertEqual(_resp.statusCode, {status})");
361 }
362
363 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
364 let lower_name = name.to_lowercase();
365 let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
366 match expected {
367 "<<present>>" => {
368 let _ = writeln!(out, " XCTAssertNotNil({header_expr})");
369 }
370 "<<absent>>" => {
371 let _ = writeln!(out, " XCTAssertNil({header_expr})");
372 }
373 "<<uuid>>" => {
374 let _ = writeln!(out, " let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
375 let _ = writeln!(
376 out,
377 " 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))"
378 );
379 }
380 exact => {
381 let escaped = escape_swift(exact);
382 let _ = writeln!(out, " XCTAssertEqual({header_expr}, \"{escaped}\")");
383 }
384 }
385 }
386
387 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
388 if let serde_json::Value::String(s) = expected {
389 let escaped = escape_swift(s);
390 let _ = writeln!(
391 out,
392 " let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
393 );
394 let _ = writeln!(
395 out,
396 " XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
397 );
398 } else {
399 let json_str = serde_json::to_string(expected).unwrap_or_default();
400 let escaped = escape_swift(&json_str);
401 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
402 let _ = writeln!(
403 out,
404 " let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
405 );
406 let _ = writeln!(
407 out,
408 " let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
409 );
410 let _ = writeln!(
411 out,
412 " XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
413 );
414 }
415 }
416
417 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
418 if let Some(obj) = expected.as_object() {
419 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
420 let _ = writeln!(
421 out,
422 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
423 );
424 for (key, val) in obj {
425 let escaped_key = escape_swift(key);
426 let swift_val = json_to_swift(val);
427 let _ = writeln!(
428 out,
429 " XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
430 );
431 }
432 }
433 }
434
435 fn render_assert_validation_errors(
436 &self,
437 out: &mut String,
438 _response_var: &str,
439 errors: &[ValidationErrorExpectation],
440 ) {
441 let _ = writeln!(out, " let _bodyData = try XCTUnwrap(_responseData)");
442 let _ = writeln!(
443 out,
444 " let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
445 );
446 let _ = writeln!(
447 out,
448 " let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
449 );
450 for ve in errors {
451 let escaped_msg = escape_swift(&ve.msg);
452 let _ = writeln!(
453 out,
454 " XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
455 );
456 }
457 }
458}
459
460fn render_http_test_method(out: &mut String, fixture: &Fixture) {
465 let Some(http) = &fixture.http else {
466 return;
467 };
468
469 if http.expected_response.status_code == 101 {
471 let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
472 let description = fixture.description.replace('"', "\\\"");
473 let _ = writeln!(out, " /// {description}");
474 let _ = writeln!(out, " func test{method_name}() throws {{");
475 let _ = writeln!(
476 out,
477 " try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
478 );
479 let _ = writeln!(out, " }}");
480 return;
481 }
482
483 client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
484}
485
486#[allow(clippy::too_many_arguments)]
491fn render_test_method(
492 out: &mut String,
493 fixture: &Fixture,
494 e2e_config: &E2eConfig,
495 _function_name: &str,
496 _result_var: &str,
497 _args: &[crate::config::ArgMapping],
498 field_resolver: &FieldResolver,
499 result_is_simple: bool,
500 enum_fields: &HashSet<String>,
501) {
502 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
504 let lang = "swift";
505 let call_overrides = call_config.overrides.get(lang);
506 let function_name = call_overrides
507 .and_then(|o| o.function.as_ref())
508 .cloned()
509 .unwrap_or_else(|| call_config.function.to_lower_camel_case());
510 let result_var = &call_config.result_var;
511 let args = &call_config.args;
512
513 let method_name = fixture.id.to_upper_camel_case();
514 let description = &fixture.description;
515 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
516 let is_async = call_config.r#async;
517
518 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, &fixture.id);
519
520 if is_async {
521 let _ = writeln!(out, " func test{method_name}() async throws {{");
522 } else {
523 let _ = writeln!(out, " func test{method_name}() throws {{");
524 }
525 let _ = writeln!(out, " // {description}");
526
527 for line in &setup_lines {
528 let _ = writeln!(out, " {line}");
529 }
530
531 if expects_error {
532 if is_async {
533 let _ = writeln!(out, " do {{");
538 let _ = writeln!(out, " _ = try await {function_name}({args_str})");
539 let _ = writeln!(out, " XCTFail(\"expected to throw\")");
540 let _ = writeln!(out, " }} catch {{");
541 let _ = writeln!(out, " // success");
542 let _ = writeln!(out, " }}");
543 } else {
544 let _ = writeln!(out, " XCTAssertThrowsError(try {function_name}({args_str}))");
545 }
546 let _ = writeln!(out, " }}");
547 return;
548 }
549
550 if is_async {
551 let _ = writeln!(out, " let {result_var} = try await {function_name}({args_str})");
552 } else {
553 let _ = writeln!(out, " let {result_var} = try {function_name}({args_str})");
554 }
555
556 for assertion in &fixture.assertions {
557 render_assertion(
558 out,
559 assertion,
560 result_var,
561 field_resolver,
562 result_is_simple,
563 enum_fields,
564 );
565 }
566
567 let _ = writeln!(out, " }}");
568}
569
570fn build_args_and_setup(
572 input: &serde_json::Value,
573 args: &[crate::config::ArgMapping],
574 fixture_id: &str,
575) -> (Vec<String>, String) {
576 if args.is_empty() {
577 return (Vec::new(), String::new());
578 }
579
580 let mut setup_lines: Vec<String> = Vec::new();
581 let mut parts: Vec<String> = Vec::new();
582
583 for arg in args {
584 if arg.arg_type == "mock_url" {
585 setup_lines.push(format!(
586 "let {} = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"",
587 arg.name,
588 ));
589 parts.push(arg.name.clone());
590 continue;
591 }
592
593 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
594 let val = input.get(field);
595 match val {
596 None | Some(serde_json::Value::Null) if arg.optional => {
597 continue;
598 }
599 None | Some(serde_json::Value::Null) => {
600 let default_val = match arg.arg_type.as_str() {
601 "string" => "\"\"".to_string(),
602 "int" | "integer" => "0".to_string(),
603 "float" | "number" => "0.0".to_string(),
604 "bool" | "boolean" => "false".to_string(),
605 _ => "nil".to_string(),
606 };
607 parts.push(default_val);
608 }
609 Some(v) => {
610 parts.push(json_to_swift(v));
611 }
612 }
613 }
614
615 (setup_lines, parts.join(", "))
616}
617
618fn render_assertion(
619 out: &mut String,
620 assertion: &Assertion,
621 result_var: &str,
622 field_resolver: &FieldResolver,
623 result_is_simple: bool,
624 enum_fields: &HashSet<String>,
625) {
626 if let Some(f) = &assertion.field {
628 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
629 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
630 return;
631 }
632 }
633
634 let field_is_enum = assertion
636 .field
637 .as_deref()
638 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
639
640 let field_expr = if result_is_simple {
641 result_var.to_string()
642 } else {
643 match &assertion.field {
644 Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
645 _ => result_var.to_string(),
646 }
647 };
648
649 let string_expr = if field_is_enum {
651 format!("{field_expr}.rawValue")
652 } else {
653 field_expr.clone()
654 };
655
656 match assertion.assertion_type.as_str() {
657 "equals" => {
658 if let Some(expected) = &assertion.value {
659 let swift_val = json_to_swift(expected);
660 if expected.is_string() {
661 let _ = writeln!(
662 out,
663 " XCTAssertEqual({string_expr}.trimmingCharacters(in: .whitespaces), {swift_val})"
664 );
665 } else {
666 let _ = writeln!(out, " XCTAssertEqual({field_expr}, {swift_val})");
667 }
668 }
669 }
670 "contains" => {
671 if let Some(expected) = &assertion.value {
672 let swift_val = json_to_swift(expected);
673 let _ = writeln!(
674 out,
675 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
676 );
677 }
678 }
679 "contains_all" => {
680 if let Some(values) = &assertion.values {
681 for val in values {
682 let swift_val = json_to_swift(val);
683 let _ = writeln!(
684 out,
685 " XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
686 );
687 }
688 }
689 }
690 "not_contains" => {
691 if let Some(expected) = &assertion.value {
692 let swift_val = json_to_swift(expected);
693 let _ = writeln!(
694 out,
695 " XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
696 );
697 }
698 }
699 "not_empty" => {
700 let _ = writeln!(
701 out,
702 " XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
703 );
704 }
705 "is_empty" => {
706 let _ = writeln!(
707 out,
708 " XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
709 );
710 }
711 "contains_any" => {
712 if let Some(values) = &assertion.values {
713 let checks: Vec<String> = values
714 .iter()
715 .map(|v| {
716 let swift_val = json_to_swift(v);
717 format!("{string_expr}.contains({swift_val})")
718 })
719 .collect();
720 let joined = checks.join(" || ");
721 let _ = writeln!(
722 out,
723 " XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
724 );
725 }
726 }
727 "greater_than" => {
728 if let Some(val) = &assertion.value {
729 let swift_val = json_to_swift(val);
730 let _ = writeln!(out, " XCTAssertGreaterThan({field_expr}, {swift_val})");
731 }
732 }
733 "less_than" => {
734 if let Some(val) = &assertion.value {
735 let swift_val = json_to_swift(val);
736 let _ = writeln!(out, " XCTAssertLessThan({field_expr}, {swift_val})");
737 }
738 }
739 "greater_than_or_equal" => {
740 if let Some(val) = &assertion.value {
741 let swift_val = json_to_swift(val);
742 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({field_expr}, {swift_val})");
743 }
744 }
745 "less_than_or_equal" => {
746 if let Some(val) = &assertion.value {
747 let swift_val = json_to_swift(val);
748 let _ = writeln!(out, " XCTAssertLessThanOrEqual({field_expr}, {swift_val})");
749 }
750 }
751 "starts_with" => {
752 if let Some(expected) = &assertion.value {
753 let swift_val = json_to_swift(expected);
754 let _ = writeln!(
755 out,
756 " XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
757 );
758 }
759 }
760 "ends_with" => {
761 if let Some(expected) = &assertion.value {
762 let swift_val = json_to_swift(expected);
763 let _ = writeln!(
764 out,
765 " XCTAssertTrue({string_expr}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
766 );
767 }
768 }
769 "min_length" => {
770 if let Some(val) = &assertion.value {
771 if let Some(n) = val.as_u64() {
772 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({field_expr}.count, {n})");
773 }
774 }
775 }
776 "max_length" => {
777 if let Some(val) = &assertion.value {
778 if let Some(n) = val.as_u64() {
779 let _ = writeln!(out, " XCTAssertLessThanOrEqual({field_expr}.count, {n})");
780 }
781 }
782 }
783 "count_min" => {
784 if let Some(val) = &assertion.value {
785 if let Some(n) = val.as_u64() {
786 let _ = writeln!(out, " XCTAssertGreaterThanOrEqual({field_expr}.count, {n})");
787 }
788 }
789 }
790 "count_equals" => {
791 if let Some(val) = &assertion.value {
792 if let Some(n) = val.as_u64() {
793 let _ = writeln!(out, " XCTAssertEqual({field_expr}.count, {n})");
794 }
795 }
796 }
797 "is_true" => {
798 let _ = writeln!(out, " XCTAssertTrue({field_expr})");
799 }
800 "is_false" => {
801 let _ = writeln!(out, " XCTAssertFalse({field_expr})");
802 }
803 "matches_regex" => {
804 if let Some(expected) = &assertion.value {
805 let swift_val = json_to_swift(expected);
806 let _ = writeln!(
807 out,
808 " XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
809 );
810 }
811 }
812 "not_error" => {
813 }
815 "error" => {
816 }
818 "method_result" => {
819 let _ = writeln!(out, " // method_result assertions not yet implemented for Swift");
820 }
821 other => {
822 panic!("Swift e2e generator: unsupported assertion type: {other}");
823 }
824 }
825}
826
827fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
833 let mut components = std::path::PathBuf::new();
834 for component in path.components() {
835 match component {
836 std::path::Component::ParentDir => {
837 if !components.as_os_str().is_empty() {
840 components.pop();
841 } else {
842 components.push(component);
843 }
844 }
845 std::path::Component::CurDir => {}
846 other => components.push(other),
847 }
848 }
849 components
850}
851
852fn json_to_swift(value: &serde_json::Value) -> String {
854 match value {
855 serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
856 serde_json::Value::Bool(b) => b.to_string(),
857 serde_json::Value::Number(n) => n.to_string(),
858 serde_json::Value::Null => "nil".to_string(),
859 serde_json::Value::Array(arr) => {
860 let items: Vec<String> = arr.iter().map(json_to_swift).collect();
861 format!("[{}]", items.join(", "))
862 }
863 serde_json::Value::Object(_) => {
864 let json_str = serde_json::to_string(value).unwrap_or_default();
865 format!("\"{}\"", escape_swift(&json_str))
866 }
867 }
868}
869
870fn escape_swift(s: &str) -> String {
872 escape_swift_str(s)
873}