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