Plugin contract for the Folk PHP application server.
Status: in active development. See folk-spec for the roadmap.
- Rust 1.85+
- Tokio async runtime
# Cargo.toml
folk-api = "0.1"A minimal plugin that logs on boot and shutdown (~25 lines):
use anyhow::Result;
use async_trait::async_trait;
use folk_api::{
Plugin, PluginContext, PluginFactory, ServerPlugin, ServerPluginWrapper,
RpcMethodDef,
};
use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, Deserialize, Default)]
struct GreetConfig {
message: Option<String>,
}
struct GreetPlugin {
message: String,
}
#[async_trait]
impl ServerPlugin for GreetPlugin {
fn name(&self) -> &'static str {
"greet"
}
async fn run(&self, ctx: PluginContext) -> Result<()> {
tracing::info!(msg = %self.message, "greet plugin running");
let mut shutdown = ctx.shutdown;
let _ = shutdown.changed().await;
tracing::info!("greet plugin stopping");
Ok(())
}
}
struct GreetFactory;
impl PluginFactory for GreetFactory {
fn create(&self, config: Value) -> Result<Box<dyn Plugin>> {
let cfg: GreetConfig = serde_json::from_value(config).unwrap_or_default();
let message = cfg.message.unwrap_or_else(|| "Hello, Folk!".into());
Ok(Box::new(ServerPluginWrapper::new(GreetPlugin { message })))
}
}
/// Required entry point — the builder calls this by name.
pub fn folk_plugin_factory() -> Box<dyn PluginFactory> {
Box::new(GreetFactory)
}Register it in folk.build.toml:
[[plugin]]
crate_name = "my_greet_plugin"
path = "../my-greet-plugin"
config_key = "greet"And configure it in folk.toml:
[greet]
message = "Howdy"Plugins receive their config section as an opaque serde_json::Value. Each plugin deserializes it into its own struct. There is no global schema — the plugin owns its config shape.
- Factory — The builder calls
folk_plugin_factory()once per plugin crate. The returnedPluginFactoryreceives the plugin's config as JSON and constructs aBox<dyn Plugin>. - Boot —
plugin.boot(ctx)is called in registration order at server startup. ReturningErris fatal: already-booted plugins are shut down in reverse order and the server exits. - Run — For
ServerPluginimplementations,run()executes as a long-lived task. Watchctx.shutdownto know when to stop. - Shutdown —
plugin.shutdown()is called in reverse registration order. Errors are logged but do not block the shutdown sequence.
Every plugin receives a PluginContext at boot:
| Field | Type | Description |
|---|---|---|
executor |
Arc<dyn Executor> |
Send work to the PHP worker pool |
shutdown |
watch::Receiver<bool> |
Fires when the server is shutting down |
rpc_registrar |
Option<Arc<dyn RpcRegistrar>> |
Register admin RPC methods |
health_registry |
Option<Arc<dyn HealthRegistry>> |
Register health checks |
metrics_registry |
Option<Arc<dyn MetricsRegistry>> |
Register Prometheus metrics |
Optional registries are None when the corresponding plugin (metrics, etc.) is not loaded. Check before use.
Plugin— 3 required methods:name(),boot(),shutdown(). Optional:rpc_methods().ServerPlugin— Convenience trait for the common "spawn a task, wait for shutdown" pattern. Wrap withServerPluginWrapperto get aPlugin.PluginFactory— Single methodcreate(config: Value) -> Result<Box<dyn Plugin>>.Executor—async fn execute(&self, payload: Bytes) -> Result<Bytes>. Sends MessagePack-encoded payloads to the PHP worker pool and returns the response.
Advertise methods from rpc_methods() and register handlers via rpc_registrar.register_raw(name, handler). Handlers receive and return Bytes (typically MessagePack-encoded).
Register via health_registry.register(name, check_fn). Return HealthStatus::ok() or HealthStatus::degraded(msg).
Create metric families via metrics_registry.counter_vec(name, help, &labels), then .with_labels(&values) to get a handle. Supports counters, gauges, and histograms. All metrics render in Prometheus text format.
See ADR 0006 — Plugin API shape for the full design rationale.
MIT