use anyhow::Result;
use camino::{Utf8Path, Utf8PathBuf};
use serde::Serialize;
use weaveffi_ir::ir::Api;
use crate::capabilities::TargetCapabilities;
use crate::model::{
BindingModel, CallbackBinding, EnumBinding, FnBinding, ListenerBinding, ModuleBinding,
StructBinding,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputFile {
pub path: Utf8PathBuf,
pub contents: String,
}
impl OutputFile {
pub fn new(path: impl Into<Utf8PathBuf>, contents: impl Into<String>) -> Self {
Self {
path: path.into(),
contents: contents.into(),
}
}
}
pub trait LanguageBackend: Send + Sync {
type Config: Serialize + Default + Clone + Send + Sync;
fn name(&self) -> &'static str;
fn capabilities(&self) -> TargetCapabilities;
fn allows_unsupported(&self, config: &Self::Config) -> bool {
let _ = config;
false
}
fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
let _ = config;
"weaveffi"
}
fn render_enum(&self, out: &mut String, e: &EnumBinding, config: &Self::Config) {
let _ = (out, e, config);
}
fn render_struct(
&self,
out: &mut String,
module: &ModuleBinding,
s: &StructBinding,
config: &Self::Config,
) {
let _ = (out, module, s, config);
}
fn render_callback(
&self,
out: &mut String,
module: &ModuleBinding,
c: &CallbackBinding,
config: &Self::Config,
) {
let _ = (out, module, c, config);
}
fn render_listener(
&self,
out: &mut String,
module: &ModuleBinding,
l: &ListenerBinding,
config: &Self::Config,
) {
let _ = (out, module, l, config);
}
fn render_function(
&self,
out: &mut String,
module: &ModuleBinding,
f: &FnBinding,
config: &Self::Config,
) {
let _ = (out, module, f, config);
}
fn emit_members(&self, out: &mut String, module: &ModuleBinding, config: &Self::Config) {
for e in &module.enums {
self.render_enum(out, e, config);
}
for s in &module.structs {
self.render_struct(out, module, s, config);
}
for c in &module.callbacks {
self.render_callback(out, module, c, config);
}
for l in &module.listeners {
self.render_listener(out, module, l, config);
}
for f in &module.functions {
self.render_function(out, module, f, config);
}
}
fn files(
&self,
api: &Api,
model: &BindingModel,
out_dir: &Utf8Path,
config: &Self::Config,
) -> Vec<OutputFile>;
}
pub fn run<B: LanguageBackend>(
backend: &B,
api: &Api,
out_dir: &Utf8Path,
config: &B::Config,
) -> Result<()> {
let model = BindingModel::build(api, backend.prefix(config));
for file in backend.files(api, &model, out_dir, config) {
if let Some(parent) = file.path.parent() {
std::fs::create_dir_all(parent.as_std_path())?;
}
std::fs::write(file.path.as_std_path(), file.contents)?;
}
Ok(())
}
fn forward_slashes(path: Utf8PathBuf) -> String {
let s = path.into_string();
if cfg!(windows) {
s.replace('\\', "/")
} else {
s
}
}
pub fn output_files<B: LanguageBackend>(
backend: &B,
api: &Api,
out_dir: &Utf8Path,
config: &B::Config,
) -> Vec<String> {
let model = BindingModel::build(api, backend.prefix(config));
let mut paths: Vec<String> = backend
.files(api, &model, out_dir, config)
.into_iter()
.map(|f| forward_slashes(f.path))
.collect();
paths.sort();
paths
}
#[doc(hidden)]
pub use anyhow as __anyhow;
#[macro_export]
macro_rules! impl_generator_via_backend {
($backend:ty) => {
impl $crate::codegen::Generator for $backend {
type Config = <$backend as $crate::backend::LanguageBackend>::Config;
fn name(&self) -> &'static str {
<$backend as $crate::backend::LanguageBackend>::name(self)
}
fn capabilities(&self) -> $crate::capabilities::TargetCapabilities {
<$backend as $crate::backend::LanguageBackend>::capabilities(self)
}
fn allows_unsupported(&self, config: &Self::Config) -> bool {
<$backend as $crate::backend::LanguageBackend>::allows_unsupported(self, config)
}
fn generate(
&self,
api: &::weaveffi_ir::ir::Api,
out_dir: &::camino::Utf8Path,
config: &Self::Config,
) -> $crate::backend::__anyhow::Result<()> {
$crate::backend::run(self, api, out_dir, config)
}
fn output_files(
&self,
api: &::weaveffi_ir::ir::Api,
out_dir: &::camino::Utf8Path,
config: &Self::Config,
) -> ::std::vec::Vec<::std::string::String> {
$crate::backend::output_files(self, api, out_dir, config)
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::Generator;
use weaveffi_ir::ir::{Function, Module, Param, TypeRef};
#[derive(Default, Clone, serde::Serialize)]
struct FakeConfig {
prefix: Option<String>,
}
struct FakeBackend;
impl LanguageBackend for FakeBackend {
type Config = FakeConfig;
fn name(&self) -> &'static str {
"fake"
}
fn capabilities(&self) -> TargetCapabilities {
TargetCapabilities::full()
}
fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
config.prefix.as_deref().unwrap_or("weaveffi")
}
fn render_enum(&self, out: &mut String, e: &EnumBinding, _c: &Self::Config) {
out.push_str(&format!("enum {}\n", e.name));
}
fn render_struct(
&self,
out: &mut String,
_m: &ModuleBinding,
s: &StructBinding,
_c: &Self::Config,
) {
out.push_str(&format!("struct {}\n", s.name));
}
fn render_function(
&self,
out: &mut String,
_m: &ModuleBinding,
f: &FnBinding,
_c: &Self::Config,
) {
let shape = match &f.shape {
crate::model::CallShape::Sync(_) => "sync",
crate::model::CallShape::Async(_) => "async",
crate::model::CallShape::Iterator(_) => "iter",
};
out.push_str(&format!("fn {} [{}] {}\n", f.name, shape, f.c_base));
}
fn files(
&self,
_api: &Api,
model: &BindingModel,
out_dir: &Utf8Path,
config: &Self::Config,
) -> Vec<OutputFile> {
let mut out = String::new();
for m in &model.modules {
out.push_str(&format!("module {}\n", m.path));
self.emit_members(&mut out, m, config);
}
vec![OutputFile::new(out_dir.join("fake/out.txt"), out)]
}
}
fn func(name: &str, returns: Option<TypeRef>, is_async: bool) -> Function {
Function {
name: name.into(),
params: vec![Param {
name: "x".into(),
ty: TypeRef::I32,
mutable: false,
doc: None,
}],
returns,
doc: None,
r#async: is_async,
cancellable: false,
deprecated: None,
since: None,
}
}
fn api() -> Api {
Api {
version: "0.3.0".into(),
modules: vec![Module {
name: "math".into(),
functions: vec![
func("add", Some(TypeRef::I32), false),
func("fetch", Some(TypeRef::StringUtf8), true),
],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
package: None,
}
}
#[test]
fn driver_walks_and_dispatches_in_canonical_order() {
let dir = tempfile::tempdir().unwrap();
let out_dir = Utf8Path::from_path(dir.path()).unwrap();
run(&FakeBackend, &api(), out_dir, &FakeConfig::default()).unwrap();
let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap();
assert_eq!(
body,
"module math\nfn add [sync] weaveffi_math_add\nfn fetch [async] weaveffi_math_fetch\n"
);
}
#[test]
fn prefix_flows_into_symbols() {
let dir = tempfile::tempdir().unwrap();
let out_dir = Utf8Path::from_path(dir.path()).unwrap();
let cfg = FakeConfig {
prefix: Some("acme".into()),
};
run(&FakeBackend, &api(), out_dir, &cfg).unwrap();
let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap();
assert!(
body.contains("acme_math_add"),
"prefix must reach symbols: {body}"
);
assert!(!body.contains("weaveffi_math_add"));
}
#[test]
fn output_files_are_sorted_paths() {
let dir = tempfile::tempdir().unwrap();
let out_dir = Utf8Path::from_path(dir.path()).unwrap();
let files = output_files(&FakeBackend, &api(), out_dir, &FakeConfig::default());
assert_eq!(files.len(), 1);
assert!(files[0].ends_with("fake/out.txt"));
}
impl_generator_via_backend!(FakeBackend);
#[test]
fn generator_bridge_delegates_to_driver() {
let dir = tempfile::tempdir().unwrap();
let out_dir = Utf8Path::from_path(dir.path()).unwrap();
let g = FakeBackend;
Generator::generate(&g, &api(), out_dir, &FakeConfig::default()).unwrap();
assert!(out_dir.join("fake/out.txt").exists());
let listed = Generator::output_files(&g, &api(), out_dir, &FakeConfig::default());
assert_eq!(listed.len(), 1);
}
}