#![allow(clippy::print_stderr)]
use crate::backends::swift::gen_bindings::boxes::emit_function_param_box_files;
use crate::backends::swift::gen_bindings::trait_bridge::{
gen_bridge_registration_overloads_file, gen_trait_bridge_files,
};
use crate::core::config::{BridgeBinding, ResolvedCrateConfig, TraitBridgeConfig};
use crate::core::ir::{
ApiSurface, EnumDef, EnumVariant, FieldDef, MethodDef, ParamDef, PrimitiveType, TypeDef, TypeRef,
};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
const RUST_BRIDGE_RUNTIME_STUB: &str = r#"import Foundation
public class RustStr {
public init() {}
public func toString() -> String { return "" }
}
public class RustString {
public init(_ value: String) { self.value = value }
public let value: String
public func toString() -> String { return value }
public func as_str() -> RustStr { return RustStr() }
}
public class RustVec<T> {
private var storage: [T] = []
public init() {}
public func push(value: T) { storage.append(value) }
public func len() -> UInt { return UInt(storage.count) }
public func get(index: UInt) -> T? { return storage[Int(index)] }
}
"#;
const RUST_BRIDGE_REGISTER_STUB: &str = r#"
public func registerDocumentSink(_ swiftBox: SwiftDocumentSinkBox) throws {}
public func unregisterDocumentSink(_ name: String) throws {}
"#;
const CLIENT_DTOS: &str = r#"import Foundation
import RustBridge
public enum PageLayout: String, Codable, Sendable {
case single
case facing
}
public struct SinkStats: Codable, Sendable {
public var accepted: UInt32
public init(accepted: UInt32) { self.accepted = accepted }
}
"#;
const CLIENT_CONFORMER: &str = r#"import Foundation
import RustBridge
public final class FixtureSink: SwiftDocumentSinkBridge {
public init() {}
public var name: String { return "fixture" }
public func version() -> String { return "0.0.0" }
public func initialize() throws {}
public func shutdown() throws {}
public func accept(chunk: String) throws {}
public func pageLayout() -> String {
return String(data: try! JSONEncoder().encode(PageLayout.facing), encoding: .utf8)!
}
public func stats() -> String {
return String(data: try! JSONEncoder().encode(SinkStats(accepted: 0)), encoding: .utf8)!
}
public func isReady() -> Bool { return false }
public func describe(layout: String) -> String { return layout }
public func statsHistory() -> [String] { return [] }
public func lastStats() -> String? { return nil }
public func latestBatch() -> String? { return nil }
public func record(entries: [String]) {}
public func sinkTotals() -> String {
return "{}"
}
}
"#;
const CLIENT_BOX_FORWARDER_STUB: &str = r#"import RustBridge
public func registerDocumentSink(_ swiftBox: SwiftDocumentSinkBox) throws {
try RustBridge.registerDocumentSink(swiftBox)
}
"#;
const DOCS_SNIPPET_CONFORMER: &str = r#"import Foundation
import Client
public final class DocsSnippetSink: SwiftDocumentSinkBridge {
public init() {}
public var name: String { return "docs-snippet" }
public func version() -> String { return "0.0.0" }
public func initialize() throws {}
public func shutdown() throws {}
public func accept(chunk: String) throws {}
public func pageLayout() -> String {
return String(data: try! JSONEncoder().encode(PageLayout.facing), encoding: .utf8)!
}
public func stats() -> String {
return String(data: try! JSONEncoder().encode(SinkStats(accepted: 0)), encoding: .utf8)!
}
public func isReady() -> Bool { return false }
public func describe(layout: String) -> String { return layout }
public func statsHistory() -> [String] { return [] }
public func lastStats() -> String? { return nil }
public func latestBatch() -> String? { return nil }
public func record(entries: [String]) {}
public func sinkTotals() -> String {
return "{}"
}
}
"#;
const PACKAGE_MANIFEST: &str = r#"// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "AlefTraitBoxFixture",
products: [
.library(name: "Client", targets: ["Client"])
],
targets: [
.target(name: "RustBridge"),
.target(name: "Client", dependencies: ["RustBridge"]),
.target(name: "DocsSnippet", dependencies: ["Client"])
]
)
"#;
fn param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
..Default::default()
}
}
fn method(name: &str, params: Vec<ParamDef>, return_type: TypeRef, error_type: Option<&str>) -> MethodDef {
MethodDef {
name: name.to_string(),
params,
return_type,
error_type: error_type.map(str::to_string),
..Default::default()
}
}
fn defaulted(mut m: MethodDef) -> MethodDef {
m.has_default_impl = true;
m
}
fn fixture_api() -> (ApiSurface, ResolvedCrateConfig) {
let trait_def = TypeDef {
name: "DocumentSink".to_string(),
rust_path: "fixture_core::DocumentSink".to_string(),
is_trait: true,
methods: vec![
method(
"accept",
vec![param("chunk", TypeRef::String)],
TypeRef::Unit,
Some("SinkError"),
),
defaulted(method(
"page_layout",
vec![],
TypeRef::Named("PageLayout".to_string()),
None,
)),
defaulted(method("stats", vec![], TypeRef::Named("SinkStats".to_string()), None)),
defaulted(method(
"is_ready",
vec![],
TypeRef::Primitive(PrimitiveType::Bool),
None,
)),
defaulted(method(
"describe",
vec![param("layout", TypeRef::Named("PageLayout".to_string()))],
TypeRef::String,
None,
)),
defaulted(method(
"stats_history",
vec![],
TypeRef::Vec(Box::new(TypeRef::Named("SinkStats".to_string()))),
None,
)),
defaulted(method(
"last_stats",
vec![],
TypeRef::Optional(Box::new(TypeRef::Named("SinkStats".to_string()))),
None,
)),
defaulted(method(
"latest_batch",
vec![],
TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named(
"SinkStats".to_string(),
))))),
None,
)),
method(
"record",
vec![param(
"entries",
TypeRef::Vec(Box::new(TypeRef::Named("SinkStats".to_string()))),
)],
TypeRef::Unit,
None,
),
defaulted(method(
"sink_totals",
vec![],
TypeRef::Map(
Box::new(TypeRef::String),
Box::new(TypeRef::Named("SinkStats".to_string())),
),
None,
)),
],
..Default::default()
};
let stats = TypeDef {
name: "SinkStats".to_string(),
rust_path: "fixture_core::SinkStats".to_string(),
has_serde: true,
fields: vec![FieldDef {
name: "accepted".to_string(),
ty: TypeRef::Primitive(PrimitiveType::U32),
..Default::default()
}],
..Default::default()
};
let page_layout = EnumDef {
name: "PageLayout".to_string(),
rust_path: "fixture_core::PageLayout".to_string(),
has_serde: true,
variants: vec![
EnumVariant {
name: "Single".to_string(),
..Default::default()
},
EnumVariant {
name: "Facing".to_string(),
..Default::default()
},
],
..Default::default()
};
let api = ApiSurface {
crate_name: "fixture_core".to_string(),
types: vec![trait_def, stats],
enums: vec![page_layout],
..Default::default()
};
let config = ResolvedCrateConfig {
name: "fixture".to_string(),
trait_bridges: vec![TraitBridgeConfig {
trait_name: "DocumentSink".to_string(),
register_fn: Some("registerDocumentSink".to_string()),
bind_via: BridgeBinding::FunctionParam,
..Default::default()
}],
..Default::default()
};
(api, config)
}
fn box_exclude(api: &ApiSurface) -> HashSet<String> {
api.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque && t.has_serde)
.map(|t| t.name.clone())
.collect()
}
fn materialize_package(root: &Path) -> std::io::Result<()> {
let (api, config) = fixture_api();
let exclude = box_exclude(&api);
let rust_bridge = root.join("Sources").join("RustBridge");
let client = root.join("Sources").join("Client");
let docs_snippet = root.join("Sources").join("DocsSnippet");
std::fs::create_dir_all(&rust_bridge)?;
std::fs::create_dir_all(&client)?;
std::fs::create_dir_all(&docs_snippet)?;
std::fs::write(root.join("Package.swift"), PACKAGE_MANIFEST)?;
std::fs::write(rust_bridge.join("SwiftBridgeCoreStub.swift"), RUST_BRIDGE_RUNTIME_STUB)?;
std::fs::write(
rust_bridge.join("RustBridgeRegisterStub.swift"),
RUST_BRIDGE_REGISTER_STUB,
)?;
std::fs::write(client.join("ClientDTOs.swift"), CLIENT_DTOS)?;
std::fs::write(client.join("FixtureSink.swift"), CLIENT_CONFORMER)?;
std::fs::write(client.join("ClientBoxForwarderStub.swift"), CLIENT_BOX_FORWARDER_STUB)?;
std::fs::write(docs_snippet.join("DocsSnippetSink.swift"), DOCS_SNIPPET_CONFORMER)?;
let trait_defs: Vec<_> = config
.trait_bridges
.iter()
.filter_map(|b| {
api.types
.iter()
.find(|t| t.is_trait && t.name == b.trait_name)
.map(|t| (b.trait_name.clone(), b, t))
})
.collect();
for (filename, content) in gen_trait_bridge_files(&trait_defs, &exclude, &HashSet::new()) {
std::fs::write(rust_bridge.join(filename), content)?;
}
for file in emit_function_param_box_files(&api, &config, &rust_bridge, &exclude) {
std::fs::write(&file.path, &file.content)?;
}
if let Some((filename, content)) = gen_bridge_registration_overloads_file(&trait_defs) {
std::fs::write(client.join(filename), content)?;
}
Ok(())
}
fn swift_driver() -> Option<PathBuf> {
if let Ok(path) = which::which("swift") {
return Some(path);
}
if cfg!(target_os = "macos") {
panic!(
"`swift` is not on PATH but this is macOS, where the Swift toolchain ships with Xcode. \
The two-target SwiftPM compile gate for the trait-box generator cannot run, and it is \
the only test that proves the generated Swift compiles. Install the toolchain rather \
than letting this gate lapse."
);
}
eprintln!(
"\n\
================================================================\n\
SKIPPED: generated_trait_box_package_compiles\n\
No `swift` driver on PATH, so the generated Swift was NOT compiled.\n\
String-level assertions alone cannot catch alef #258; this run\n\
provides NO evidence that the trait-box output builds.\n\
================================================================\n"
);
None
}
#[test]
fn generated_trait_box_package_compiles() {
let Some(swift) = swift_driver() else { return };
let keep = std::env::var_os("ALEF_SWIFT_FIXTURE_DIR").map(PathBuf::from);
let tmp = tempfile::tempdir().expect("create fixture package dir");
let root = keep.as_deref().unwrap_or_else(|| tmp.path());
materialize_package(root).expect("write fixture SwiftPM package");
let output = Command::new(&swift)
.arg("build")
.arg("--package-path")
.arg(root)
.arg("--scratch-path")
.arg(root.join(".build"))
.output()
.expect("run swift build");
assert!(
output.status.success(),
"generated trait-box package failed to compile in the Client -> RustBridge layout.\n\
--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}