Skip to main content

Crate lattice_plugin_sdk

Crate lattice_plugin_sdk 

Source
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-time NAME + DOC with MessagePack encode / decode.
  • #[derive(PluginEvent)] — derives all four from a serde struct: DOC from the struct’s /// doc-comment (the doc-comment IS the event doc), NAME from #[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 — shape and 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§

DecodeError
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).
OptionParseError
A failed parse_option — the get-option string didn’t parse for the option’s value type. Carries the underlying parser message.

Enums§

OptionKind
The value type of a plugin option — the WIT-agnostic mirror of the config interface’s option-type enum. The plugin maps this to the generated option-type at the register-option call site (the SDK can’t name the per-world WIT type — approach A).

Traits§

PluginEvent
A plugin-defined event: a typed view over the opaque emit-event / on-event wire (PH7.8b.2). Implement via #[derive(PluginEvent)] on a serde-serializable struct; hand-implementing is possible but rarely needed.
PluginOption
A plugin-defined scalar option — a typed view over the config register/read wire (slice PH7.10b). Implement via #[derive(PluginOption)] on a newtype over bool / i64 / String; #[option(default = "...")] is required, #[option(name = "...")] defaults to the kebab-cased type name. For structured (record / list / enum) options use shape::ConfigShape instead.

Functions§

parse_option
Parse a get-option result string into the option’s typed value (slice PH7.10b). get-option returns the value formatted by the native OptionType; this reads it back into O::Value via its FromStr.
try_decode
Subscriber-side helper: if name names event E, decode payload into it; otherwise None (the event is for a different subscriber). Folds the name-gate the guest would otherwise write by hand in on-event into one call.

Derive Macros§

ConfigShape
Derive ConfigShape for a struct with named fields, or for an enum whose variants are all unit.
PluginEvent
Derive PluginEvent for a serde-serializable struct. See the crate docs for the generated items and the #[event(name = "...")] / doc-comment inputs.
PluginOption
Derive PluginOption for a newtype struct over bool / i64 / String (PH7.10b) — the guest-side ergonomic layer over the config.register-option wire. Like PluginEvent, it is WIT-agnostic: it generates only metadata constants + the value type; the plugin makes the register-option / get-option WIT calls itself using them.