use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions::toolchain;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::FixtureGroup;
pub(super) const SWIFT_FORMAT_IGNORE_DIRECTIVE: &str = "// swift-format-ignore-file\n\n";
pub(super) fn render_test_helpers_swift() -> String {
let header = hash::header(CommentStyle::DoubleSlash);
let ignore = SWIFT_FORMAT_IGNORE_DIRECTIVE;
format!(
r#"{header}{ignore}import Foundation
#if canImport(FoundationNetworking)
// URLSession, URLRequest, HTTPURLResponse, and URLSessionTaskDelegate live in
// the FoundationNetworking submodule on swift-corelibs-foundation (Linux). On
// Apple platforms these types remain in plain Foundation and this submodule
// does not exist; the canImport guard skips the import there.
import FoundationNetworking
#endif
import RustBridge
// Make `RustString` print its content in XCTest failure output. Without this,
// every error thrown from the swift-bridge layer surfaces as
// `caught error: "RustBridge.RustString"` with the actual message hidden
// inside the opaque class instance. The `@retroactive` keyword acknowledges
// that the conformed-to protocol (`CustomStringConvertible`) and the
// conforming type (`RustString`) both live outside this module — required by
// Swift 6 to silence the retroactive-conformance warning. swift-bridge does
// not give `RustString` a `description` of its own, so there is no conflict.
extension RustString: @retroactive CustomStringConvertible {{
public var description: String {{ self.toString() }}
}}
// URLSession delegate that does not follow redirects, so tests can assert on 3xx status codes
// and Location headers instead of transparently chasing them to the final response.
final class AlefE2ENoRedirectDelegate: NSObject, URLSessionTaskDelegate {{
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void
) {{
completionHandler(nil)
}}
}}
// Mock server base URL accessor used by the generated test bodies.
// The `MOCK_SERVER_URL` env var is exported by `scripts/e2e/run-with-mock-server.sh`
// (which spawns the `mock-server` binary built from `e2e/rust`) before invoking
// `swift test`. We fall back to a `localhost` URL that will fail-fast at request
// time so misconfigured runs surface a clear error instead of silently hitting
// production endpoints.
enum AlefE2EMockServer {{
static var baseURL: String {{
ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]
?? "http://127.0.0.1:0"
}}
}}
"#
)
}
fn chunk_fixtures_for_swift(json: &str) -> Vec<String> {
const CHUNK_SIZE: usize = 30000;
let mut max_hash_run = 0;
let mut current_run = 0;
for c in json.chars() {
if c == '#' {
current_run += 1;
max_hash_run = max_hash_run.max(current_run);
} else {
current_run = 0;
}
}
let delimiter_level = max_hash_run + 1;
let delimiter = "#".repeat(delimiter_level);
let mut chunks = Vec::new();
let mut current_chunk = String::new();
for c in json.chars() {
if !current_chunk.is_empty() && current_chunk.len() + c.len_utf8() > CHUNK_SIZE {
chunks.push(format!("{0}\"{1}\"{0}", delimiter, current_chunk));
current_chunk.clear();
}
current_chunk.push(c);
}
if !current_chunk.is_empty() {
chunks.push(format!("{0}\"{1}\"{0}", delimiter, current_chunk));
}
chunks
}
pub(super) fn render_app_harness(e2e_config: &E2eConfig, groups: &[FixtureGroup], module_name: &str) -> String {
let mut fixtures_map = serde_json::Map::new();
for group in groups {
for fixture in &group.fixtures {
if fixture.http.is_none() {
continue;
}
let http_data = &fixture.http.as_ref().unwrap();
let fixture_json = serde_json::json!({
"http": {
"handler": {
"route": &http_data.handler.route,
"method": &http_data.handler.method,
"body_schema": http_data.handler.body_schema.clone(),
},
"request": {
"path": &http_data.request.path,
},
"expected_response": {
"status_code": http_data.expected_response.status_code,
"body": &http_data.expected_response.body,
"headers": &http_data.expected_response.headers,
}
}
});
fixtures_map.insert(fixture.id.clone(), fixture_json);
}
}
let fixtures_json = serde_json::to_string(&fixtures_map).unwrap_or_default();
let fixtures_json_chunks = chunk_fixtures_for_swift(&fixtures_json);
let host = &e2e_config.harness.host;
let port = e2e_config.harness.port;
let app_class = e2e_config.harness.app_class_for_lang("swift");
let register_route_method = e2e_config
.harness
.register_method_idiomatic("swift")
.unwrap_or_else(|| "registerRoute".to_string());
let body_schema_setter = &e2e_config.harness.body_schema_setter;
let method_enum = &e2e_config.harness.method_enum;
let run_method = e2e_config.harness.run_method_for_lang("swift");
let header = hash::header(CommentStyle::DoubleSlash);
let mut imports = e2e_config.harness.imports_for_lang("swift");
if !imports.iter().any(|i| i.to_lowercase() == module_name.to_lowercase()) {
imports.insert(0, module_name.to_string());
}
let imports_str = imports
.iter()
.map(|m| format!("import {}", m))
.collect::<Vec<_>>()
.join("\n");
let ctx = minijinja::context! {
header => header,
imports => imports_str,
app_class => app_class.as_deref().unwrap_or("App"),
route_builder_constructor => "RouteBuilder",
route_builder_schema_setter => body_schema_setter.as_deref().unwrap_or("requestSchemaJson"),
method_enum_class => method_enum.as_deref().unwrap_or("Method"),
register_route_method => register_route_method.as_str(),
run_method => run_method.as_deref().unwrap_or("run"),
response_body_field => e2e_config.harness.response_body_field.as_str(),
host => host,
port => port,
fixtures_json_chunks => fixtures_json_chunks,
};
crate::e2e::template_env::render("swift/app_harness.swift.jinja", ctx)
}
pub(super) fn render_package_swift(
module_name: &str,
registry_url: &str,
pkg_path: &str,
pkg_version: &str,
dep_mode: crate::e2e::config::DependencyMode,
include_harness_target: bool,
) -> String {
let min_macos = toolchain::SWIFT_MIN_MACOS;
let (dependencies_block, test_target_dep) = match dep_mode {
crate::e2e::config::DependencyMode::Registry => {
let github_repo_url = registry_url.trim_end_matches(".git");
let package_dep = format!(
r#" .package(url: "{github_repo_url}", from: "{pkg_version}"),
"#
);
let deps_block = format!(" dependencies: [\n{package_dep} ],\n");
let pkg_id = github_repo_url
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or(module_name)
.trim_end_matches(".git");
let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
(deps_block, prod)
}
crate::e2e::config::DependencyMode::Local => {
let pkg_id = pkg_path.trim_end_matches('/').rsplit('/').next().unwrap_or(module_name);
let deps_block = format!(" dependencies: [\n .package(path: \"{pkg_path}\"),\n ],\n");
let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
(deps_block, prod)
}
};
let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
let min_ios = toolchain::SWIFT_MIN_IOS;
let min_ios_major = min_ios.split('.').next().unwrap_or(min_ios);
let harness_target = if include_harness_target {
format!(
r#" .executableTarget(
name: "Harness",
dependencies: [{test_target_dep}],
path: "Sources/Harness"
),
"#
)
} else {
String::new()
};
let targets_block = format!(
r#"{harness_target} .testTarget(
name: "{module_name}E2ETests",
dependencies: [{test_target_dep}]
),
"#
);
format!(
r#"// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "E2eSwift",
platforms: [
.macOS(.v{min_macos_major}),
.iOS(.v{min_ios_major}),
],
{dependencies_block} targets: [
{targets_block} ]
)
"#
)
}