folk-api 0.1.1

Plugin contract for the Folk PHP application server
Documentation
# folk-api

Plugin contract for the Folk PHP application server.

> **Status:** in active development. See [folk-spec]https://github.com/Folk-Project/folk-spec for the roadmap.

## Requirements

- Rust 1.85+
- Tokio async runtime

## Installation

```toml
# Cargo.toml
folk-api = "0.1"
```

## Quick start

A minimal plugin that logs on boot and shutdown (~25 lines):

```rust
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`:

```toml
[[plugin]]
crate_name = "my_greet_plugin"
path = "../my-greet-plugin"
config_key = "greet"
```

And configure it in `folk.toml`:

```toml
[greet]
message = "Howdy"
```

## Configuration

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.

## How it works

### Plugin lifecycle

1. **Factory** — The builder calls `folk_plugin_factory()` once per plugin crate. The returned `PluginFactory` receives the plugin's config as JSON and constructs a `Box<dyn Plugin>`.
2. **Boot**`plugin.boot(ctx)` is called in registration order at server startup. Returning `Err` is fatal: already-booted plugins are shut down in reverse order and the server exits.
3. **Run** — For `ServerPlugin` implementations, `run()` executes as a long-lived task. Watch `ctx.shutdown` to know when to stop.
4. **Shutdown**`plugin.shutdown()` is called in reverse registration order. Errors are logged but do not block the shutdown sequence.

### PluginContext

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.

### Key traits

- **`Plugin`** — 3 required methods: `name()`, `boot()`, `shutdown()`. Optional: `rpc_methods()`.
- **`ServerPlugin`** — Convenience trait for the common "spawn a task, wait for shutdown" pattern. Wrap with `ServerPluginWrapper` to get a `Plugin`.
- **`PluginFactory`** — Single method `create(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.

### RPC methods

Advertise methods from `rpc_methods()` and register handlers via `rpc_registrar.register_raw(name, handler)`. Handlers receive and return `Bytes` (typically MessagePack-encoded).

### Health checks

Register via `health_registry.register(name, check_fn)`. Return `HealthStatus::ok()` or `HealthStatus::degraded(msg)`.

### Metrics

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](https://github.com/Folk-Project/folk-spec/blob/main/adr/0006-plugin-api-shape.md) for the full design rationale.

## License

MIT