Expand description
The guest-side Rust SDK for lattice plugins: typed event payloads, typed options and typed configuration shapes layered over the plugin-host WIT wire. Compiled INTO plugins (Rust today; other component-model languages use the WIT directly), never into the host.
§What it owns
The WIT is the plugin API; this crate adds zero capability that is not
on the wire — only ergonomics a Rust author would otherwise hand-write. The
plugin-host emit-event / register-event host-services (PH7.8b.2) carry
name: string + payload: list<u8> — opaque MessagePack the host never
interprets. That is deliberate (the boundary discipline the whole host rests
on), but raw bytes are a poor author API. This crate adds the type-safe layer:
PluginEvent— a trait pairing a compile-timeNAME+DOCwith MessagePackencode/decode.#[derive(PluginEvent)]— derives all four from a serde struct:DOCfrom the struct’s///doc-comment (the doc-comment IS the event doc),NAMEfrom#[event(name = "...")]or the kebab-cased type name.try_decode— the subscriber-side helper: name-gate + decode in one.PluginOption+#[derive(PluginOption)]+parse_option— the same shape for scalar options (bool/i64/String).shape—shape::ConfigShape+#[derive(ConfigShape)]: a Rust struct as a structured config schema and value, and the arena flattening (shape::flatten_schema,shape::flatten_value,shape::unflatten_value) the WIT seam needs.
§What it must not depend on
No lattice-* runtime crate and no wit-bindgen bindings — only serde,
rmp-serde and its own derive. Two structural reasons: it is published and
versioned for out-of-tree plugin authors, so it cannot drag the editor in;
and it must compose with EVERY plugin world, which it can only do by naming
none of their generated types. It is a separate crate because it is the one
piece of lattice that compiles into guests.
§WIT-agnostic by design (approach A)
This crate touches no plugin-host bindings — it is pure serde + a derive.
The host calls stay plugin-side one-liners using the derived constants
(host_services below stands in for a plugin’s generated bindings):
use lattice_plugin_sdk::{DecodeError, PluginEvent};
use serde::{Deserialize, Serialize};
/// The indexer finished scanning a file.
#[derive(Debug, PartialEq, Serialize, Deserialize, PluginEvent)]
#[event(name = "indexer.file-scanned")]
struct FileScanned {
path: String,
symbols: u32,
}
// at register-events:
host_services::register_event(FileScanned::NAME, FileScanned::DOC);
// to emit:
let ev = FileScanned { path: "src/lib.rs".into(), symbols: 42 };
let payload = ev.encode();
host_services::emit_event(FileScanned::NAME, &payload);
// in another plugin's on-event(name, payload):
if let Some(ev) = lattice_plugin_sdk::try_decode::<FileScanned>(name, payload) {
let ev = ev?; // a real FileScanned
return Ok(Some(ev));
}
assert_eq!(on_event("indexer.file-scanned", &payload), Ok(Some(ev)));
assert_eq!(FileScanned::DOC, "The indexer finished scanning a file.");Because the SDK is world-agnostic it composes with EVERY plugin world (events,
grammar, completion, …) unchanged — it is the seed the other SDK seams reuse.
A fuller ctx.emit(ev) / on_event::<E>() sugar can layer on once a real
multi-world plugin exists to shape the host-call binding.
§Cross-plugin contracts
Because a PluginEvent type is just a serde struct, plugin A can publish its
event types in a shared crate and plugin B can depend on it — a
compile-checked, versioned event contract (the coordinating-plugins use case).
§Design
docs/dev/architecture/plugin-host.md— the host the wire talks to, and the events / config seams this crate types.docs/dev/architecture/typed-configuration.md—shapeand the arena encoding.docs/dev/guides/plugin-authoring.md— end-to-end plugin authoring.
Modules§
- shape
- A Rust struct as a config schema and a config value, plus the arena flattening the WIT seam needs (slice TC.4).
Structs§
- Decode
Error - A failed
PluginEvent::decode— the payload was not valid MessagePack for the target type (wrong event type, version skew, corruption). Carries the underlying decoder message; opaque and stable (it hides the serde impl). - Option
Parse Error - A failed
parse_option— theget-optionstring didn’t parse for the option’s value type. Carries the underlying parser message.
Enums§
- Option
Kind - The value type of a plugin option — the WIT-agnostic mirror of the
configinterface’soption-typeenum. The plugin maps this to the generatedoption-typeat theregister-optioncall site (the SDK can’t name the per-world WIT type — approach A).
Traits§
- Plugin
Event - A plugin-defined event: a typed view over the opaque
emit-event/on-eventwire (PH7.8b.2). Implement via#[derive(PluginEvent)]on a serde-serializable struct; hand-implementing is possible but rarely needed. - Plugin
Option - A plugin-defined scalar option — a typed view over the
configregister/read wire (slice PH7.10b). Implement via#[derive(PluginOption)]on a newtype overbool/i64/String;#[option(default = "...")]is required,#[option(name = "...")]defaults to the kebab-cased type name. For structured (record / list / enum) options useshape::ConfigShapeinstead.
Functions§
- parse_
option - Parse a
get-optionresult string into the option’s typed value (slice PH7.10b).get-optionreturns the value formatted by the nativeOptionType; this reads it back intoO::Valuevia itsFromStr. - try_
decode - Subscriber-side helper: if
namenames eventE, decodepayloadinto it; otherwiseNone(the event is for a different subscriber). Folds the name-gate the guest would otherwise write by hand inon-eventinto one call.
Derive Macros§
- Config
Shape - Derive
ConfigShapefor a struct with named fields, or for an enum whose variants are all unit. - Plugin
Event - Derive
PluginEventfor a serde-serializable struct. See the crate docs for the generated items and the#[event(name = "...")]/ doc-comment inputs. - Plugin
Option - Derive
PluginOptionfor a newtype struct overbool/i64/String(PH7.10b) — the guest-side ergonomic layer over theconfig.register-optionwire. LikePluginEvent, it is WIT-agnostic: it generates only metadata constants + the value type; the plugin makes theregister-option/get-optionWIT calls itself using them.