tocat_api/plugin.rs
1//! The plugin traits and the contexts they are driven through.
2//!
3//! The lifecycle is four calls and two contexts. [`PluginFactory::build`] runs
4//! once per direction per connection with a [`BuildCtx`]: this is where config
5//! is deserialized, side channels are reserved, and anything derived from the
6//! stage's fixed position is cached. [`Plugin::on_bytes`] then runs per chunk
7//! with a [`Ctx`], [`Plugin::on_tick`] runs on a schedule the stage asks for,
8//! and [`Plugin::on_eof`] once at the end: the last chance for a stage holding
9//! buffered bytes to emit them, and where a codec writes its epilogue.
10//!
11//! The split between the two contexts is the point: everything expensive or
12//! fallible belongs to build time, so the per-chunk path is a synchronous call
13//! that either forwards a slice or writes into a buffer.
14//!
15//! A call emits one unit by default, however many times it forwards: the
16//! pieces concatenate, and the host delivers them as one write. A stage that
17//! needs them kept apart says so with [`Ctx::boundary`], which is what turns a
18//! stage that merely accumulates bytes into one that records them.
19
20use std::time::Duration;
21
22use serde::{Deserialize, Serialize, de::DeserializeOwned};
23use serde_json::{Map, Value};
24/// Severity of a record a stage asked the host to log.
25///
26/// The same type a guest writes into its outbox, since a level that crossed
27/// the WebAssembly boundary and a level a native plugin passed to
28/// [`Ctx::log`] are the same thing.
29pub use tocat_wasm_abi::Level as LogLevel;
30pub use tocat_wasm_abi::*;
31
32use crate::{
33 Direction,
34 channel::{ChannelId, ChannelTarget, HostBuilder},
35 error::{PluginError, Result},
36 forgiving::Forgiving,
37};
38
39/// Where the host runs a stage.
40///
41/// `Inline` stages run on the reading task: one synchronous call per chunk, no
42/// channel, no wakeup, and (see [`Ctx::pass_through`]) no copy. `Detached`
43/// buys concurrency with the reader at the cost of one copy and one task hop
44/// per chunk, so it pays off only for stages doing real work per byte:
45/// compression, encryption, parsing, etc.
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "kebab-case")]
48pub enum Execution {
49 #[default]
50 Inline,
51 Detached,
52}
53
54/// Collects the side effects a plugin asks for during one call.
55///
56/// The host implements this over per-channel staging buffers, so a side write
57/// is an `extend_from_slice` and nothing else. Staged bytes are flushed to
58/// their sinks concurrently with the downstream write.
59pub trait EffectSink {
60 fn write(&mut self, channel: ChannelId, bytes: &[u8]);
61 /// `stage` is the emitting stage's display name, so the host can tag the
62 /// line without every plugin having to repeat itself.
63 fn log(&mut self, level: LogLevel, stage: &str, message: &str);
64
65 /// Wait `delay` before reading upstream again.
66 ///
67 /// The one effect that acts on the reader rather than on a side channel. A
68 /// stage cannot sleep: it is synchronous, it runs on the reading task, and
69 /// a guest has no runtime to sleep on. So it asks, and the host holds off
70 /// its next read. Several stages asking on one chunk get the longest of
71 /// the requests, not the sum.
72 ///
73 /// Defaults to doing nothing, so a host that has no reader to hold (a test
74 /// harness, an offline driver) is not obliged to honour it.
75 fn pace(&mut self, delay: Duration) {
76 let _ = delay;
77 }
78
79 /// Stop reading upstream, as if it had just reached end of stream.
80 ///
81 /// Everything already emitted is still written, `on_eof` still cascades,
82 /// and the path closes down its normal way. This is how a stage ends a
83 /// transfer deliberately, which is not a failure and must not be reported
84 /// as one.
85 fn halt(&mut self, stage: &str, reason: &str) {
86 let _ = (stage, reason);
87 }
88}
89
90/// Static description of the path a pipeline instance sits on.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct PipelineMeta {
93 pub direction: Direction,
94 pub source: String,
95 pub sink: String,
96 pub peer: Option<String>,
97}
98
99impl PipelineMeta {
100 pub fn new(direction: Direction, source: impl Into<String>, sink: impl Into<String>) -> Self {
101 Self {
102 direction,
103 source: source.into(),
104 sink: sink.into(),
105 peer: None,
106 }
107 }
108
109 #[must_use]
110 pub fn with_peer(mut self, peer: Option<impl Into<String>>) -> Self {
111 self.peer = peer.map(Into::into);
112 self
113 }
114
115 /// The endpoint bytes are read from on this path.
116 #[must_use]
117 pub fn upstream(&self) -> &str {
118 match self.direction {
119 Direction::SourceToSink => &self.source,
120 Direction::SinkToSource => &self.sink,
121 }
122 }
123
124 /// The endpoint bytes are written to on this path.
125 #[must_use]
126 pub fn downstream(&self) -> &str {
127 match self.direction {
128 Direction::SourceToSink => &self.sink,
129 Direction::SinkToSource => &self.source,
130 }
131 }
132
133 /// `"source -> sink"`, oriented for this path.
134 #[must_use]
135 pub fn label(&self) -> String {
136 format!("{} -> {}", self.upstream(), self.downstream())
137 }
138}
139
140/// Where a stage sits in its pipeline, and what it is called.
141///
142/// `upstream` and `downstream` are the stage's actual neighbours on this path:
143/// the adjacent stages' display names, or an endpoint name at either end. A
144/// `tee` wedged between two other stages therefore describes the hop it is
145/// really watching rather than the endpoints it is nowhere near.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct StageInfo<'a> {
148 /// Position on this path, after direction filtering and mirroring.
149 pub index: usize,
150 pub total: usize,
151 /// The `as = "..."` alias if one was given, otherwise the plugin name,
152 /// suffixed with `#n` when the same name appears more than once.
153 pub name: &'a str,
154 pub upstream: &'a str,
155 pub downstream: &'a str,
156}
157
158impl StageInfo<'_> {
159 /// `"upstream -> downstream"`, oriented for this path.
160 #[must_use]
161 pub fn label(&self) -> String {
162 format!("{} -> {}", self.upstream, self.downstream)
163 }
164
165 #[must_use]
166 pub fn is_first(&self) -> bool {
167 self.index == 0
168 }
169
170 #[must_use]
171 pub fn is_last(&self) -> bool {
172 self.index + 1 == self.total
173 }
174}
175
176/// What one call to a stage produced: the bytes it emitted, how they are
177/// framed, and what it asked the host to do about its own schedule.
178///
179/// One struct rather than four borrows because they are written together and
180/// read together, and because a stage's whole answer is exactly these four
181/// things. The host keeps one per buffer and resets it between calls, so a
182/// steady stream allocates nothing here after the first few chunks.
183#[derive(Debug, Default)]
184pub struct Emission {
185 /// Bytes the stage wrote. Empty when it passed through or emitted nothing.
186 pub(crate) out: Vec<u8>,
187 /// One offset per unit, each the end of its unit. Empty means unframed,
188 /// which is one unit covering everything.
189 pub(crate) bounds: Vec<usize>,
190 pub(crate) emit: Emit,
191 /// Whether the stage asked for its tick schedule to be restarted.
192 pub(crate) rearm: bool,
193}
194
195impl Emission {
196 #[must_use]
197 pub fn new() -> Self {
198 Self::default()
199 }
200
201 /// Ready for a fresh call, keeping the allocations.
202 pub fn reset(&mut self) {
203 self.out.clear();
204 self.bounds.clear();
205 self.emit = Emit::Pending;
206 self.rearm = false;
207 }
208
209 /// Ready for the next unit of a framed call.
210 ///
211 /// What has been emitted so far stays, because units concatenate into one
212 /// buffer, but the decision is per unit. A rearm request is not cleared
213 /// either: it is about the stage, not about the unit.
214 pub(crate) fn next_unit(&mut self) {
215 self.emit = Emit::Pending;
216 }
217
218 /// Close the unit left open, so `bounds` always ends where `out` does.
219 ///
220 /// Does nothing when no bytes have arrived since the last boundary, so a
221 /// stage cannot declare an empty unit, and nothing at all on an empty
222 /// emission. Note that this frames an emission that had declared no
223 /// framing, so the host calls it only where that is what it means.
224 pub(crate) fn close(&mut self) {
225 if self.bounds.last().copied().unwrap_or(0) < self.out.len() {
226 self.bounds.push(self.out.len());
227 }
228 }
229
230 /// Everything the stage emitted, concatenated.
231 #[must_use]
232 pub fn bytes(&self) -> &[u8] {
233 &self.out
234 }
235
236 /// The framing of [`bytes`](Self::bytes). Empty means one unit.
237 #[must_use]
238 pub fn bounds(&self) -> &[usize] {
239 &self.bounds
240 }
241
242 /// What the stage did with what it was given.
243 #[must_use]
244 pub fn emit(&self) -> Emit {
245 self.emit
246 }
247
248 /// Whether the stage asked for its schedule to be restarted. See
249 /// [`Ctx::rearm`].
250 #[must_use]
251 pub fn rearm_requested(&self) -> bool {
252 self.rearm
253 }
254}
255
256/// Handed to a plugin for each chunk.
257///
258/// A stage must say what happens to the chunk:
259/// [`pass_through`](Self::pass_through) forwards it untouched,
260/// [`forward`](Self::forward) emits different bytes, and doing neither drops
261/// it. Passthrough is the fast path and costs nothing (the next stage receives
262/// the same slice).
263///
264/// Several calls to `forward` in one turn emit one unit, not several: the
265/// bytes concatenate and are delivered together. [`boundary`](Self::boundary)
266/// is how a stage says otherwise.
267pub struct Ctx<'a> {
268 meta: &'a PipelineMeta,
269 stage: &'a str,
270 input: &'a [u8],
271 emission: &'a mut Emission,
272 sink: &'a mut dyn EffectSink,
273}
274
275impl<'a> Ctx<'a> {
276 pub fn new(
277 meta: &'a PipelineMeta,
278 stage: &'a str,
279 input: &'a [u8],
280 emission: &'a mut Emission,
281 sink: &'a mut dyn EffectSink,
282 ) -> Self {
283 Self {
284 meta,
285 stage,
286 input,
287 emission,
288 sink,
289 }
290 }
291
292 /// This stage's display name, as it appears in logs.
293 #[must_use]
294 pub fn stage(&self) -> &str {
295 self.stage
296 }
297
298 #[must_use]
299 pub fn meta(&self) -> &PipelineMeta {
300 self.meta
301 }
302
303 #[must_use]
304 pub fn direction(&self) -> Direction {
305 self.meta.direction
306 }
307
308 /// The chunk this call was given. Empty during [`Plugin::on_eof`].
309 #[must_use]
310 pub fn input(&self) -> &[u8] {
311 self.input
312 }
313
314 /// Forward the input unchanged, without copying it.
315 pub fn pass_through(&mut self) {
316 match self.emission.emit {
317 Emit::Pending => self.emission.emit = Emit::Passthrough,
318 Emit::Passthrough => {}
319 // Something was emitted already, so passthrough has to materialise.
320 Emit::Buffered => {
321 let input = self.input;
322 self.emission.out.extend_from_slice(input);
323 }
324 }
325 }
326
327 /// Emit `bytes` downstream. Performs a copy. Use
328 /// [`pass_through`](Self::pass_through) when the bytes are the input.
329 ///
330 /// Appends: calling this twice emits both, in order, as one unit. Call
331 /// [`boundary`](Self::boundary) between them to emit two.
332 pub fn forward(&mut self, bytes: &[u8]) {
333 if self.emission.emit == Emit::Passthrough {
334 let input = self.input;
335 self.emission.out.extend_from_slice(input);
336 }
337
338 self.emission.emit = Emit::Buffered;
339 self.emission.out.extend_from_slice(bytes);
340 }
341
342 /// Explicitly swallow the chunk. Emitting nothing does the same thing; this
343 /// exists so a filter can state the intent.
344 pub fn drop_chunk(&mut self) {
345 if self.emission.emit == Emit::Passthrough {
346 self.emission.emit = Emit::Pending;
347 }
348 }
349
350 /// End the current unit. What was forwarded since the last boundary is
351 /// delivered on its own: one write at a byte sink, one message at a
352 /// datagram sink, one parcel across a detached boundary, and one
353 /// [`on_bytes`](Plugin::on_bytes) call at every stage below.
354 ///
355 /// Only worth calling when those splits are the point, as with a stage
356 /// cutting a stream into fixed-size records. Framing is not free: each
357 /// stage below is then called once per unit rather than once per chunk, so
358 /// a stage that emits many small units is asking the rest of the segment
359 /// to run many times. A stage that only rewrites bytes should leave the
360 /// framing it was given alone and say nothing.
361 ///
362 /// The trailing unit does not need one: whatever is forwarded after the
363 /// last boundary is closed automatically, so no bytes can be lost by
364 /// forgetting.
365 ///
366 /// Ignored when nothing has been forwarded since the last call, so a stage
367 /// cannot emit an empty unit by accident.
368 pub fn boundary(&mut self) {
369 if self.emission.emit == Emit::Passthrough {
370 let input = self.input;
371 self.emission.out.extend_from_slice(input);
372 self.emission.emit = Emit::Buffered;
373 }
374
375 self.emission.close();
376 }
377
378 /// Restart this stage's tick schedule: the next
379 /// [`on_tick`](Plugin::on_tick) falls a full interval from now rather than
380 /// wherever the existing cadence happens to land.
381 ///
382 /// A stage cannot read a clock, so it cannot measure how long it has been
383 /// holding something. What it can do is say when the waiting started, and
384 /// this is how. A stage that begins accumulating calls this, and its next
385 /// tick then means "an interval since you asked" rather than "an interval
386 /// since some earlier moment you know nothing about".
387 ///
388 /// Without it, [`tick_interval`](Plugin::tick_interval) is a cadence
389 /// rather than a delay: a tick that came due while bytes were flowing
390 /// fires at the next opportunity, which can be immediately after the bytes
391 /// it is about arrived.
392 ///
393 /// Cheap to call, and harmless to call often: it sets a flag the host
394 /// reads once at the end of the call. Ignored for a stage that asked for
395 /// no ticks.
396 pub fn rearm(&mut self) {
397 self.emission.rearm = true;
398 }
399
400 /// Stage bytes for a side channel obtained from [`BuildCtx::open_channel`].
401 pub fn side_write(&mut self, channel: ChannelId, bytes: &[u8]) {
402 self.sink.write(channel, bytes);
403 }
404
405 pub fn log(&mut self, level: LogLevel, message: &str) {
406 self.sink.log(level, self.stage, message);
407 }
408
409 /// Ask the host to wait `delay` before reading upstream again.
410 ///
411 /// Applied after this call returns and after whatever was emitted has been
412 /// written, so the bytes in hand are never held hostage by the wait. On a
413 /// socket this is real backpressure: the read stops, the receive buffer
414 /// fills, the window closes and the peer slows down. Nothing is buffered
415 /// on this side.
416 pub fn pace(&mut self, delay: Duration) {
417 self.sink.pace(delay);
418 }
419
420 /// Ask the host to stop reading upstream, as if it had reached end of
421 /// stream. `reason` is logged against this stage.
422 pub fn halt(&mut self, reason: &str) {
423 self.sink.halt(self.stage, reason);
424 }
425}
426
427/// Handed to a [`PluginFactory`] while constructing one instance.
428pub struct BuildCtx<'a> {
429 name: &'a str,
430 config: &'a Map<String, Value>,
431 meta: &'a PipelineMeta,
432 stage: StageInfo<'a>,
433 host: &'a mut dyn HostBuilder,
434}
435
436impl<'a> BuildCtx<'a> {
437 pub fn new(
438 name: &'a str,
439 config: &'a Map<String, Value>,
440 meta: &'a PipelineMeta,
441 stage: StageInfo<'a>,
442 host: &'a mut dyn HostBuilder,
443 ) -> Self {
444 Self {
445 name,
446 config,
447 meta,
448 stage,
449 host,
450 }
451 }
452
453 /// Where this instance sits in the pipeline. Cache anything derived from
454 /// it as the position cannot change after construction.
455 #[must_use]
456 pub fn stage(&self) -> StageInfo<'a> {
457 self.stage
458 }
459
460 #[must_use]
461 pub fn name(&self) -> &str {
462 self.name
463 }
464
465 #[must_use]
466 pub fn meta(&self) -> &PipelineMeta {
467 self.meta
468 }
469
470 #[must_use]
471 pub fn direction(&self) -> Direction {
472 self.meta.direction
473 }
474
475 #[must_use]
476 pub fn raw_config(&self) -> &Map<String, Value> {
477 self.config
478 }
479
480 /// Deserialize the entry's options into the plugin's own config type.
481 ///
482 /// Through [`Forgiving`], so option keys and enum values are matched the
483 /// way every other identifier in tocat is: case-insensitively, with dashes
484 /// and underscores treated as noise. A plugin declares its config exactly
485 /// as it would otherwise, including `deny_unknown_fields`, and needs to
486 /// know nothing about this.
487 pub fn config<T: DeserializeOwned>(&self) -> Result<T> {
488 T::deserialize(Forgiving(Value::Object(self.config.clone())))
489 .map_err(|e| PluginError::config(self.name, e))
490 }
491
492 /// Reserve a side channel. Equal targets share one handle process-wide.
493 pub fn open_channel(&mut self, target: ChannelTarget) -> Result<ChannelId> {
494 self.host.open_channel(target)
495 }
496}
497
498/// What a factory produced.
499///
500/// Most stages are [`Plugin`]s the host calls per chunk. A few cannot be: a
501/// subprocess decides nothing synchronously, may emit nothing for the chunk it
502/// was given, and may emit bytes belonging to three chunks ago. Rather than
503/// bend [`Plugin`] into something a subprocess could satisfy (and lose the
504/// property that makes it portable to WASM) such a stage is *described* here
505/// and *run* by the host.
506///
507/// A WASM guest can only ever produce [`Stage::Filter`]; spawning is a host
508/// capability by construction.
509pub enum Stage {
510 Filter(Box<dyn Plugin>),
511 External(ExternalStage),
512}
513
514impl Stage {
515 /// Build a filter stage from a plugin
516 pub fn filter(plugin: impl Plugin + 'static) -> Self {
517 Self::Filter(Box::new(plugin))
518 }
519}
520
521impl From<Box<dyn Plugin>> for Stage {
522 fn from(plugin: Box<dyn Plugin>) -> Self {
523 Self::Filter(plugin)
524 }
525}
526
527/// A subprocess to run as a stage, with the relay's bytes on its stdin and its
528/// stdout continuing downstream.
529///
530/// Always its own segment: it cannot share a task with inline stages, and
531/// `detach = false` on one is a contradiction.
532#[derive(Debug, Clone, PartialEq, Eq)]
533pub struct ExternalStage {
534 /// Program and arguments, or a single shell command when `shell` is set.
535 pub argv: Vec<String>,
536 pub shell: bool,
537 pub stderr: StderrMode,
538 /// Display name, for logs and for attributing the child's stderr.
539 pub name: String,
540}
541
542/// What to do with a child's stderr.
543#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
544#[serde(rename_all = "kebab-case")]
545pub enum StderrMode {
546 /// Forward to the relay's own stderr. Interleaves with dumps and logs.
547 Inherit,
548 /// Capture and re-emit as warnings tagged with the stage name.
549 #[default]
550 Log,
551 Null,
552}
553
554/// One stage of a pipeline. Instances are per-direction and per-connection.
555///
556/// Synchronous on purpose: it is the only shape that maps onto a WASM guest
557/// call, and it keeps the per-chunk cost at a function call rather than a
558/// future poll. Anything that must await belongs on the effect side, where the
559/// host performs it off the critical path.
560pub trait Plugin: Send {
561 /// The name of the plugin
562 fn name(&self) -> &str;
563
564 /// A chunk arrived from upstream. `input` is the same slice as
565 /// [`Ctx::input`]; it is passed separately because it is the hot argument.
566 ///
567 /// One call is one unit. Where a stage above declared framing with
568 /// [`Ctx::boundary`] that is one call per unit rather than one per chunk,
569 /// so a stage never has to unpick two of them from a single slice.
570 fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()>;
571
572 /// Upstream reached EOF. Last chance to emit buffered bytes.
573 fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
574 let _ = ctx;
575 Ok(())
576 }
577
578 /// How often this stage wants [`on_tick`](Plugin::on_tick) called, or
579 /// `None` (the default) for never.
580 ///
581 /// Read once, at the end of construction, so it must not depend on
582 /// anything that changes later. A stage whose interval is configurable
583 /// reads its config in [`PluginFactory::build`] and answers from that.
584 ///
585 /// The host owns the clock. A guest cannot read one (a WASM module has no
586 /// way to reach the host's time) which is why this is a period the stage
587 /// *asks for* rather than a timestamp it checks. The cost falls on the
588 /// relay: one timer per direction per connection for any pipeline
589 /// containing a ticking stage, so a stage asking for milliseconds is
590 /// asking every forked connection to wake up that often.
591 fn tick_interval(&self) -> Option<Duration> {
592 None
593 }
594
595 /// The stage's schedule came due.
596 ///
597 /// Called from the same task, and under the same rules, as
598 /// [`on_bytes`](Plugin::on_bytes), it just arrives without any. This is
599 /// how a stage does anything that time rather than traffic should drive:
600 /// report a measurement, release bytes it has been holding back, emit a
601 /// keepalive. Without it a stalled stream and a finished one are
602 /// indistinguishable from inside a plugin.
603 ///
604 /// [`Ctx::input`] is empty, so there is nothing to pass through; anything
605 /// emitted here is emitted with [`Ctx::forward`] and continues downstream
606 /// through the stages *below* this one, in the same way
607 /// [`on_eof`](Plugin::on_eof) cascades. Emitting nothing is the common
608 /// case and costs nothing.
609 ///
610 /// A stage that emits from here is fabricating a message boundary on a
611 /// datagram path (the bytes belong to no datagram the peer sent) so it
612 /// should report [`Boundaries::Fuse`] from
613 /// [`boundaries`](Plugin::boundaries). A stage that only observes need
614 /// not.
615 ///
616 /// What is emitted here is one unit unless [`Ctx::boundary`] says
617 /// otherwise, exactly as in [`on_bytes`](Plugin::on_bytes).
618 ///
619 /// Ticks run for the life of the pipeline and stop at end of stream, so
620 /// they arrive whether or not anything is moving, which is the point, and
621 /// is what a keepalive needs. A stage that has nothing to say until the
622 /// first chunk has arrived is expected to keep that state itself.
623 fn on_tick(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
624 let _ = ctx;
625 Ok(())
626 }
627
628 /// What this stage does to the message boundaries passing through it.
629 ///
630 /// On a byte stream a chunk is an arbitrary slice: a stage may buffer,
631 /// split or coalesce freely, and the host is free to do the same. On a
632 /// datagram path the chunk *is* the message: one `on_bytes` call per
633 /// datagram, and whatever it emits is sent as exactly one datagram. A
634 /// stage that buffers across calls, or emits two messages' worth from one,
635 /// silently corrupts the protocol unless it says so here.
636 ///
637 /// The four answers, in the order a stage usually wants them:
638 ///
639 /// - [`Preserve`](Boundaries::Preserve): one unit in, one unit out. Every
640 /// observer, and every codec that rewrites a message in place.
641 /// - [`Fuse`](Boundaries::Fuse): the units are gone below this stage.
642 /// Anything that buffers across calls, splits, coalesces, or emits from a
643 /// tick.
644 /// - [`Seal`](Boundaries::Seal): as `Preserve`, and the boundary is also
645 /// written into the payload, so it outlives a stage below that fuses.
646 /// `frame` and nothing else.
647 /// - [`Split`](Boundaries::Split): the units below are read out of the
648 /// bytes rather than inherited, so the ones from above do not survive.
649 /// `unframe` and nothing else.
650 ///
651 /// Defaults to `Fuse` because that is the answer that claims nothing,
652 /// which is the safe one for a stage that has not thought about it,
653 /// including any plugin loaded from outside this binary.
654 ///
655 /// Declaring the truth matters more than declaring safety. `block` fuses
656 /// and says so, and it is still the right stage to reach for when one
657 /// datagram per 1400 bytes is exactly what was wanted: the host warns and
658 /// relays anyway.
659 fn boundaries(&self) -> Boundaries {
660 Boundaries::Fuse
661 }
662
663 /// What this stage needs of the path it was placed on.
664 ///
665 /// Unlike [`boundaries`](Plugin::boundaries), which the host only warns
666 /// about, an unmet requirement is a build error: a stage saying this
667 /// cannot do its job at all where it was put.
668 ///
669 /// [`Upstream`](Needs::Upstream) means every call must carry one whole
670 /// message, so boundaries have to arrive from a datagram endpoint or from
671 /// an `unframe`. [`Downstream`](Needs::Downstream) means the units this
672 /// stage emits have to reach a datagram endpoint or a `frame`, or what it
673 /// wrote cannot be read back. The two are separate because the stages
674 /// that want them want opposite ones: a stage that seals a message and
675 /// appends a tag makes its own boundaries and needs them to survive
676 /// downwards, while the stage that verifies and strips that tag needs
677 /// whole messages from above and does not care what happens below it.
678 ///
679 /// Read once, after `build`, alongside `boundaries`. Neither is consulted
680 /// on the per-chunk path.
681 fn needs(&self) -> Needs {
682 Needs::Nothing
683 }
684}
685
686/// Constructs [`Plugin`] instances from a declared entry.
687pub trait PluginFactory: Send + Sync + 'static {
688 fn name(&self) -> &str;
689
690 fn description(&self) -> &str {
691 ""
692 }
693
694 /// Default placement. Overridable per entry with `detach = true|false`.
695 fn execution(&self) -> Execution {
696 Execution::Inline
697 }
698
699 fn build(&self, ctx: &mut BuildCtx<'_>) -> Result<Stage>;
700}