use std::path::Path;
use anyhow::{Context, Result};
pub fn run(input_dir: &Path, output_dir: &Path) -> Result<()> {
std::fs::create_dir_all(output_dir)?;
let mut count = 0;
for entry in std::fs::read_dir(input_dir)
.with_context(|| format!("cannot read input dir: {}", input_dir.display()))?
.flatten()
{
let path = entry.path();
if path.extension().is_some_and(|e| e == "js") {
let code = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let methods = extract_fn_names(&code);
let plugin_id = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let swift = generate_swift_plugin(&plugin_id, &methods);
let struct_name = pascal_case(&plugin_id);
let out = output_dir.join(format!("{struct_name}Plugin.swift"));
std::fs::write(&out, swift).with_context(|| format!("write {}", out.display()))?;
println!(
" {} ({} methods) → {}",
plugin_id,
methods.len(),
out.display()
);
count += 1;
}
}
println!("generated {count} Swift plugin wrapper(s)");
Ok(())
}
pub fn extract_fn_names(code: &str) -> Vec<String> {
let mut names = Vec::new();
for line in code.lines() {
let trimmed = line.trim();
let rest = if let Some(r) = trimmed.strip_prefix("export function ") {
r
} else if let Some(r) = trimmed.strip_prefix("function ") {
r
} else {
continue;
};
let name: String = rest
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
names.push(name);
}
}
names
}
fn generate_swift_plugin(plugin_id: &str, methods: &[String]) -> String {
let struct_name = pascal_case(plugin_id);
let mut out = String::new();
out.push_str("// AUTO-GENERATED by `aurorality bindgen` — do not edit\n");
out.push_str("// Source: ");
out.push_str(plugin_id);
out.push_str(".js\n\n");
out.push_str("import Foundation\n\n");
out.push_str("/// Swift wrapper for the `");
out.push_str(plugin_id);
out.push_str("` JavaScript plugin.\n");
out.push_str("/// Load the plugin first:\n");
out.push_str("/// try loadJsPlugin(id: \"");
out.push_str(plugin_id);
out.push_str("\", resource: \"");
out.push_str(plugin_id);
out.push_str("\")\n");
out.push_str("public struct ");
out.push_str(&struct_name);
out.push_str("Plugin {\n");
out.push_str(" public static let pluginId = \"");
out.push_str(plugin_id);
out.push_str("\"\n\n");
out.push_str(" private let bridge: AurorBridge\n\n");
out.push_str(" public init(bridge: AurorBridge) {\n");
out.push_str(" self.bridge = bridge\n");
out.push_str(" }\n");
for method in methods {
out.push('\n');
out.push_str(" /// Call the `");
out.push_str(method);
out.push_str("` function in `");
out.push_str(plugin_id);
out.push_str(".js`.\n");
out.push_str(" /// `payload` must be a valid JSON object string, e.g. `\"{\\\"count\\\": 1}\"`.\n");
out.push_str(" public func ");
out.push_str(method);
out.push_str("(_ payload: String = \"{}\") throws -> String {\n");
out.push_str(" try bridge.invoke(\n");
out.push_str(" pluginId: Self.pluginId,\n");
out.push_str(" method: \"");
out.push_str(method);
out.push_str("\",\n");
out.push_str(" payload: payload\n");
out.push_str(" )\n");
out.push_str(" }\n");
}
out.push_str("}\n");
out
}
pub fn pascal_case(s: &str) -> String {
let mut result = String::new();
let mut cap_next = true;
for ch in s.chars() {
if ch == '-' || ch == '_' {
cap_next = true;
} else if cap_next {
result.extend(ch.to_uppercase());
cap_next = false;
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_named_fns() {
let code = r#"
export function increment(payload) {
return { count: payload.count + 1 };
}
function decrement(payload) {
return { count: payload.count - 1 };
}
const helper = () => {};
"#;
let names = extract_fn_names(code);
assert_eq!(names, vec!["increment", "decrement"]);
}
#[test]
fn pascal_cases() {
assert_eq!(pascal_case("counter"), "Counter");
assert_eq!(pascal_case("my-plugin"), "MyPlugin");
assert_eq!(pascal_case("my_plugin"), "MyPlugin");
}
}