Skip to main content

folk_builder/
codegen.rs

1use crate::config::{BuildConfig, PluginEntry};
2
3pub fn generate_cargo_toml(cfg: &BuildConfig) -> String {
4    let mut deps = String::new();
5
6    if let Some(path) = &cfg.build.folk_ext_path {
7        deps.push_str(&format!(
8            "folk-ext = {{ path = \"{}\", default-features = false }}\n",
9            path
10        ));
11    } else {
12        deps.push_str("folk-ext = { version = \"0.2\", default-features = false }\n");
13    }
14
15    deps.push_str(
16        r#"folk-api = "0.2"
17serde_json = "1"
18toml = "0.8"
19ext-php-rs = "0.15"
20bytes = "1"
21anyhow = "1"
22"#,
23    );
24
25    for plugin in &cfg.plugin {
26        deps.push_str(&generate_plugin_dep(plugin));
27    }
28
29    let patch_section = if let Some(path) = &cfg.build.folk_api_path {
30        format!(
31            "\n[patch.crates-io]\nfolk-api = {{ path = \"{}\" }}\n",
32            path
33        )
34    } else {
35        String::new()
36    };
37
38    format!(
39        r#"[package]
40name = "{output}"
41version = "0.1.0"
42edition = "2024"
43
44[lib]
45name = "{output}"
46crate-type = ["cdylib"]
47
48[dependencies]
49{deps}
50[build-dependencies]
51ext-php-rs-build = "0.1"
52{patch}"#,
53        output = cfg.build.output,
54        deps = deps,
55        patch = patch_section
56    )
57}
58
59fn generate_plugin_dep(p: &PluginEntry) -> String {
60    if let Some(path) = &p.path {
61        format!("{} = {{ path = \"{}\" }}\n", p.crate_name, path)
62    } else if let Some(git) = &p.git {
63        let ver = p.version.as_deref().unwrap_or("0.1");
64        format!(
65            "{} = {{ git = \"{}\", version = \"{}\" }}\n",
66            p.crate_name, git, ver
67        )
68    } else {
69        let ver = p.version.as_deref().unwrap_or("0.1");
70        format!("{} = \"{}\"\n", p.crate_name, ver)
71    }
72}
73
74pub fn generate_lib_rs(cfg: &BuildConfig) -> String {
75    let imports: Vec<String> = cfg
76        .plugin
77        .iter()
78        .map(|p| {
79            let ident = p.crate_name.replace('-', "_");
80            format!("use {}::folk_plugin_factory as {}_factory;", ident, ident)
81        })
82        .collect();
83
84    let registrations: Vec<String> = cfg
85        .plugin
86        .iter()
87        .map(|p| {
88            let ident = p.crate_name.replace('-', "_");
89            let key = if p.config_key.is_empty() {
90                &p.crate_name
91            } else {
92                &p.config_key
93            };
94            format!(
95                r#"    let cfg_{ident} = raw_cfg.get("{key}").cloned().unwrap_or(toml::Value::Table(Default::default()));
96    let cfg_{ident}_json = serde_json::to_value(&cfg_{ident})?;
97    plugins.push({ident}_factory().create(cfg_{ident}_json)?);
98"#,
99            )
100        })
101        .collect();
102
103    format!(
104        r#"//! Generated by folk-builder. Do not edit.
105//! Plugins: {plugin_list}
106
107use ext_php_rs::binary::Binary;
108use ext_php_rs::prelude::*;
109{imports}
110
111fn create_plugins(config_path: &str) -> anyhow::Result<(folk_ext::folk_core::config::FolkConfig, Vec<Box<dyn folk_api::Plugin>>)> {{
112    let config = folk_ext::folk_core::config::FolkConfig::load_from(config_path)?;
113    let raw_cfg: toml::Table = {{
114        let content = std::fs::read_to_string(config_path).unwrap_or_default();
115        content.parse().unwrap_or_else(|_| toml::Table::new())
116    }};
117    let mut plugins: Vec<Box<dyn folk_api::Plugin>> = Vec::new();
118{registrations}
119    Ok((config, plugins))
120}}
121
122// --- PHP wrappers ---
123
124#[php_class]
125#[php(name = "Folk\\Server")]
126#[derive(Debug)]
127pub struct FolkServer {{
128    config_path: String,
129}}
130
131#[php_impl]
132impl FolkServer {{
133    pub fn __construct(config_path: String) -> Self {{
134        Self {{ config_path }}
135    }}
136
137    pub fn start(&self) -> PhpResult<()> {{
138        let (config, plugins) = create_plugins(&self.config_path)
139            .map_err(|e| PhpException::default(format!("Config error: {{e}}")))?;
140        folk_ext::start_server(config, plugins)
141            .map_err(|e| PhpException::default(format!("Start error: {{e}}")))?;
142        Ok(())
143    }}
144}}
145
146#[php_function]
147pub fn folk_version() -> String {{
148    folk_ext::version()
149}}
150
151#[php_function]
152pub fn folk_call(method: String, payload: Binary<u8>) -> PhpResult<Binary<u8>> {{
153    let data: Vec<u8> = payload.into();
154    let result = folk_ext::call_method(&method, bytes::Bytes::from(data))
155        .map_err(|e| PhpException::default(format!("folk_call({{method}}): {{e}}")))?;
156    Ok(Binary::new(result.to_vec()))
157}}
158
159#[php_function]
160pub fn folk_worker_ready() -> PhpResult<bool> {{
161    folk_ext::bridge::do_ready()
162        .map_err(|e| PhpException::default(format!("folk_worker_ready: {{e}}")))
163}}
164
165#[php_function]
166pub fn folk_worker_recv() -> PhpResult<Option<Vec<Binary<u8>>>> {{
167    match folk_ext::bridge::do_recv() {{
168        Ok(Some((method, payload))) => {{
169            Ok(Some(vec![Binary::new(method.into_bytes()), Binary::new(payload)]))
170        }},
171        Ok(None) => Ok(None),
172        Err(e) => Err(PhpException::default(format!("folk_worker_recv: {{e}}"))),
173    }}
174}}
175
176#[php_function]
177pub fn folk_worker_send(result: Binary<u8>) -> PhpResult<()> {{
178    let data: Vec<u8> = result.into();
179    folk_ext::bridge::do_send(&data)
180        .map_err(|e| PhpException::default(format!("folk_worker_send: {{e}}")))
181}}
182
183#[php_function]
184pub fn folk_worker_send_error(message: String) -> PhpResult<()> {{
185    folk_ext::bridge::do_send_error(&message)
186        .map_err(|e| PhpException::default(format!("folk_worker_send_error: {{e}}")))
187}}
188
189#[php_function]
190pub fn folk_is_worker_thread() -> bool {{
191    folk_ext::bridge::has_worker_state()
192}}
193
194#[php_function]
195pub fn folk_worker_run(dispatch_fn: String) -> PhpResult<()> {{
196    folk_ext::bridge::run_dispatch_loop(&dispatch_fn)
197        .map_err(|e| PhpException::default(format!("folk_worker_run: {{e}}")))
198}}
199
200#[php_module]
201pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{
202    module
203        .class::<FolkServer>()
204        .function(wrap_function!(folk_version))
205        .function(wrap_function!(folk_call))
206        .function(wrap_function!(folk_worker_ready))
207        .function(wrap_function!(folk_worker_recv))
208        .function(wrap_function!(folk_worker_send))
209        .function(wrap_function!(folk_worker_send_error))
210        .function(wrap_function!(folk_is_worker_thread))
211        .function(wrap_function!(folk_worker_run))
212}}
213"#,
214        imports = imports.join("\n"),
215        registrations = registrations.join(""),
216        plugin_list = cfg
217            .plugin
218            .iter()
219            .map(|p| p.crate_name.as_str())
220            .collect::<Vec<_>>()
221            .join(", ")
222    )
223}
224
225pub fn generate_build_rs() -> String {
226    r#"use ext_php_rs_build::{ApiVersion, PHPInfo, emit_check_cfg, emit_php_cfg_flags, find_php};
227
228fn main() {
229    let php = find_php().expect("Failed to find PHP");
230    let info = PHPInfo::get(&php).expect("Failed to get PHP info");
231    let version: ApiVersion = info
232        .zend_version()
233        .expect("Failed to get Zend version")
234        .try_into()
235        .expect("Unsupported PHP version");
236    emit_php_cfg_flags(version);
237    emit_check_cfg();
238
239    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
240    if target_os == "macos" {
241        println!("cargo:rustc-cdylib-link-arg=-undefined");
242        println!("cargo:rustc-cdylib-link-arg=dynamic_lookup");
243    }
244}
245"#
246    .to_string()
247}