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