tocat_api/channel.rs
1//! Side channels: the only way a plugin reaches the outside world.
2//!
3//! A plugin never opens a file. At build time it describes the sink it wants
4//! and receives an opaque [`ChannelId`]. At run time it queues writes against
5//! that id. The host owns the file descriptor, dedupes identical targets so two
6//! plugins pointing at the same path share one buffered writer, and can refuse
7//! a target outright (see stdout, below).
8
9use std::path::PathBuf;
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::Result;
14
15/// Opaque handle to a host-owned side channel.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
17pub struct ChannelId(pub u32);
18
19impl ChannelId {
20 #[must_use]
21 pub fn index(self) -> usize {
22 self.0 as usize
23 }
24}
25
26/// Where a side channel points.
27///
28/// Note the absence of stdout: on a `-` / stdio endpoint that stream carries
29/// relay payload, and interleaving dump output into it would corrupt the
30/// transfer. Hosts are expected to reject it.
31#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(tag = "type", rename_all = "kebab-case")]
33pub enum ChannelTarget {
34 Stderr,
35 File {
36 path: PathBuf,
37 #[serde(default)]
38 append: bool,
39 },
40}
41
42impl ChannelTarget {
43 /// Create a File ChannelTarget from something path-like
44 pub fn file(path: impl Into<PathBuf>) -> Self {
45 Self::File {
46 path: path.into(),
47 append: true,
48 }
49 }
50}
51
52/// Implemented by the host. Handed to plugins during construction only.
53pub trait HostBuilder {
54 /// Reserve a side channel, returning a handle to write to later.
55 ///
56 /// Implementations should return the same [`ChannelId`] for equal targets.
57 fn open_channel(&mut self, target: ChannelTarget) -> Result<ChannelId>;
58}