use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::PathBuf,
};
use bevy_app::{App, Plugin, Startup};
use bevy_ecs::prelude::*;
use bevy_ecs::reflect::AppFunctionRegistry;
use crate::methods::FunctionIndex;
#[derive(Resource, Clone, Debug)]
pub struct WitGeneratorSettings {
pub package: String,
pub interface: String,
pub world: String,
pub wasvy_package: String,
pub output_path: PathBuf,
}
impl Default for WitGeneratorSettings {
fn default() -> Self {
Self {
package: "game:components".to_string(),
interface: "components".to_string(),
world: "host".to_string(),
wasvy_package: "wasvy:ecs".to_string(),
output_path: PathBuf::from("target/wasvy/components.wit"),
}
}
}
#[derive(Default)]
pub struct WitGeneratorPlugin {
settings: WitGeneratorSettings,
}
impl WitGeneratorPlugin {
pub fn new(settings: WitGeneratorSettings) -> Self {
Self { settings }
}
}
impl Plugin for WitGeneratorPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(self.settings.clone())
.add_systems(Startup, write_wit);
}
}
fn write_wit(
settings: Res<WitGeneratorSettings>,
type_registry: Res<AppTypeRegistry>,
function_registry: Res<AppFunctionRegistry>,
) {
let output = generate_wit(&settings, &type_registry, &function_registry);
if let Some(parent) = settings.output_path.parent()
&& let Err(err) = fs::create_dir_all(parent)
{
bevy_log::error!("Failed to create WIT output dir: {err}");
return;
}
if let Err(err) = fs::write(&settings.output_path, output) {
bevy_log::error!("Failed to write WIT file: {err}");
}
}
#[derive(Default)]
struct ComponentEntry {
name: String,
type_path: String,
methods: Vec<MethodEntry>,
}
#[derive(Clone)]
struct MethodEntry {
name: String,
arg_names: Vec<String>,
arg_types: Vec<String>,
ret: String,
}
pub fn generate_wit(
settings: &WitGeneratorSettings,
type_registry: &AppTypeRegistry,
function_registry: &AppFunctionRegistry,
) -> String {
let index = FunctionIndex::build(type_registry, function_registry);
let mut components: BTreeMap<String, ComponentEntry> = BTreeMap::new();
for type_path in index.components() {
let entry = components.entry(type_path.to_string()).or_default();
entry.type_path = type_path.to_string();
if entry.name.is_empty() {
entry.name = type_path_to_name(type_path);
}
}
for type_path in index.components() {
for method in index.methods_for(type_path) {
let entry = components.entry(type_path.to_string()).or_default();
entry.methods.push(MethodEntry {
name: method.method.clone(),
arg_names: method.args.iter().map(|arg| arg.name.clone()).collect(),
arg_types: method
.args
.iter()
.map(|arg| arg.type_path.clone())
.collect(),
ret: method.ret.clone(),
});
}
}
render_wit(settings, components)
}
fn render_wit(
settings: &WitGeneratorSettings,
components: BTreeMap<String, ComponentEntry>,
) -> String {
let mut out = String::new();
out.push_str(&format!("package {};\n\n", settings.package));
out.push_str(&format!("interface {} {{\n", settings.interface));
out.push_str(&format!(
" use {}/app.{{component}};\n\n",
settings.wasvy_package
));
let mut used_names = BTreeSet::new();
for (type_path, mut entry) in components {
if entry.name.is_empty() {
entry.name = type_path_to_name(&type_path);
}
if entry.type_path.is_empty() {
entry.type_path = type_path.clone();
}
let resource_name = to_wit_ident(&entry.name, &mut used_names);
out.push_str(&format!(" /// wasvy:type-path={}\n", entry.type_path));
out.push_str(&format!(" resource {} {{\n", resource_name));
out.push_str(" constructor(component: component);\n");
for method in entry.methods {
let signature = render_method(&method);
out.push_str(&format!(" {};\n", signature));
}
out.push_str(" }\n");
}
out.push_str("}\n\n");
out.push_str(&format!("world {} {{\n", settings.world));
out.push_str(&format!(" import {};\n", settings.interface));
out.push_str("}\n");
out
}
fn render_method(method: &MethodEntry) -> String {
let mut args = Vec::new();
for (name, ty) in method.arg_names.iter().zip(method.arg_types.iter()) {
let mapped = map_type(ty);
args.push(format!("{}: {}", name, mapped));
}
let args = args.join(", ");
let ret = map_type(&method.ret);
if ret == "()" {
format!("{}: func({})", method.name, args)
} else {
format!("{}: func({}) -> {}", method.name, args, ret)
}
}
fn type_path_to_name(type_path: &str) -> String {
type_path
.rsplit("::")
.next()
.unwrap_or(type_path)
.to_string()
}
fn to_wit_ident(name: &str, used: &mut BTreeSet<String>) -> String {
let mut out = String::new();
let mut prev_lower = false;
for ch in name.chars() {
if ch == '_' {
out.push('-');
prev_lower = false;
continue;
}
if ch.is_ascii_uppercase() {
if prev_lower {
out.push('-');
}
out.push(ch.to_ascii_lowercase());
prev_lower = false;
} else {
out.push(ch.to_ascii_lowercase());
prev_lower = ch.is_ascii_lowercase();
}
}
if out.is_empty() {
out.push_str("component");
}
let mut candidate = out.clone();
let mut index = 1;
while used.contains(&candidate) {
candidate = format!("{out}-{index}");
index += 1;
}
used.insert(candidate.clone());
candidate
}
fn map_type(ty: &str) -> String {
let ty = ty.trim();
if ty == "()" {
return "()".to_string();
}
let ty = ty.replace(' ', "");
if let Some(inner) = strip_generic(&ty, "Option") {
return format!("option<{}>", map_type(inner));
}
if let Some(inner) = strip_generic(&ty, "Vec") {
return format!("list<{}>", map_type(inner));
}
match strip_path(&ty) {
"bool" => "bool".to_string(),
"u8" => "u8".to_string(),
"u16" => "u16".to_string(),
"u32" => "u32".to_string(),
"u64" => "u64".to_string(),
"i8" => "s8".to_string(),
"i16" => "s16".to_string(),
"i32" => "s32".to_string(),
"i64" => "s64".to_string(),
"f32" => "f32".to_string(),
"f64" => "f64".to_string(),
"String" | "str" => "string".to_string(),
other => unimplemented!("Type '{other}' has no known representation in wit"),
}
}
fn strip_path(ty: &str) -> &str {
ty.rsplit("::").next().unwrap_or(ty)
}
fn strip_generic<'a>(ty: &'a str, name: &str) -> Option<&'a str> {
let simple = strip_path(ty);
if !simple.starts_with(name) {
return None;
}
let start = simple.find('<')?;
let end = simple.rfind('>')?;
if end <= start + 1 {
return None;
}
Some(&simple[start + 1..end])
}
#[cfg(test)]
mod tests {
use super::*;
use bevy_app::App;
use bevy_ecs::component::Component;
use bevy_reflect::Reflect;
#[derive(Component, Reflect, Default)]
struct Health {
current: f32,
max: f32,
}
impl Health {
fn heal(&mut self, amount: f32) {
self.current = (self.current + amount).min(self.max);
}
fn pct(&self) -> f32 {
self.current / self.max
}
}
#[test]
fn generates_wit() {
let mut app = App::new();
app.register_type::<Health>();
app.register_type_data::<Health, crate::authoring::WasvyExport>();
app.register_function(Health::heal);
app.register_function(Health::pct);
let settings = WitGeneratorSettings::default();
let type_registry = app
.world()
.get_resource::<AppTypeRegistry>()
.expect("AppTypeRegistry");
let function_registry = app
.world()
.get_resource::<AppFunctionRegistry>()
.expect("AppFunctionRegistry");
let output = generate_wit(&settings, type_registry, function_registry);
let wasvy_use = "use wasvy:ecs/app.{component}";
assert!(output.contains(wasvy_use));
assert!(output.contains("resource health"));
assert!(output.contains("wasvy:type-path="));
assert!(output.contains("constructor(component: component)"));
assert!(output.contains("heal: func(arg0: f32)"));
assert!(output.contains("pct: func() -> f32"));
assert!(output.contains("world host"));
}
}