2026-06-23 15:42:28 +00:00
|
|
|
//! Extension contract for typed datastream payloads.
|
|
|
|
|
//!
|
|
|
|
|
//! The pipe owns frames, ordering, transport, ingest, and storage. It does not
|
|
|
|
|
//! own the universe of channel meanings. Producers and consumers define records
|
|
|
|
|
//! in their own crates by implementing [`Record`], then optionally compose a
|
2026-07-12 06:14:34 +00:00
|
|
|
//! [`ChannelRegistry`] when a view needs to render payload bytes by channel name.
|
2026-06-23 15:42:28 +00:00
|
|
|
|
|
|
|
|
use std::collections::BTreeSet;
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
/// How a view should treat a channel's payload bytes.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum ChannelKind {
|
|
|
|
|
/// Decode as structured JSON for display.
|
|
|
|
|
Typed,
|
|
|
|
|
/// Decode as UTF-8 text for display.
|
|
|
|
|
Text,
|
|
|
|
|
/// Unknown to this consumer; preserve and render as raw bytes.
|
|
|
|
|
Opaque,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// Caller-owned channel-name classifier used by views.
|
2026-06-23 15:42:28 +00:00
|
|
|
pub trait ChannelClassifier {
|
2026-07-12 06:14:34 +00:00
|
|
|
fn classify(&self, channel_name: &str) -> ChannelKind;
|
2026-06-23 15:42:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<F> ChannelClassifier for F
|
|
|
|
|
where
|
2026-07-12 06:14:34 +00:00
|
|
|
F: Fn(&str) -> ChannelKind,
|
2026-06-23 15:42:28 +00:00
|
|
|
{
|
2026-07-12 06:14:34 +00:00
|
|
|
fn classify(&self, channel_name: &str) -> ChannelKind {
|
|
|
|
|
self(channel_name)
|
2026-06-23 15:42:28 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A typed channel record: crates define their own records and bind each one to
|
2026-07-12 06:14:34 +00:00
|
|
|
/// the channel name it rides on.
|
2026-06-23 15:42:28 +00:00
|
|
|
pub trait Record: Serialize + for<'de> Deserialize<'de> + Sized {
|
2026-07-12 06:14:34 +00:00
|
|
|
/// The concrete channel name this record is carried on.
|
2026-06-23 15:42:28 +00:00
|
|
|
const CHANNEL: &'static str;
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// The channel name this record is carried on.
|
|
|
|
|
fn channel_name() -> &'static str {
|
|
|
|
|
Self::CHANNEL
|
2026-06-23 15:42:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Encode this record to opaque payload bytes.
|
|
|
|
|
fn encode(&self) -> Vec<u8> {
|
|
|
|
|
serde_json::to_vec(self).expect("record serializes to JSON")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decode payload bytes back into the record.
|
|
|
|
|
fn decode(payload: &[u8]) -> Result<Self, serde_json::Error> {
|
|
|
|
|
serde_json::from_slice(payload)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Small composable classifier for consumers that want registry-style decoding
|
|
|
|
|
/// without putting a global catalog inside `datastream`.
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
pub struct ChannelRegistry {
|
|
|
|
|
typed: BTreeSet<String>,
|
|
|
|
|
text: BTreeSet<String>,
|
|
|
|
|
text_prefixes: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChannelRegistry {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self::default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_typed_channel(mut self, channel: impl Into<String>) -> Self {
|
|
|
|
|
self.typed.insert(channel.into());
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_record<R: Record>(self) -> Self {
|
|
|
|
|
self.with_typed_channel(R::CHANNEL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_text_channel(mut self, channel: impl Into<String>) -> Self {
|
|
|
|
|
self.text.insert(channel.into());
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_text_prefix(mut self, prefix: impl Into<String>) -> Self {
|
|
|
|
|
self.text_prefixes.push(prefix.into());
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
pub fn classify_name(&self, channel: &str) -> ChannelKind {
|
|
|
|
|
if self.typed.contains(channel) {
|
2026-06-23 15:42:28 +00:00
|
|
|
ChannelKind::Typed
|
2026-07-12 06:14:34 +00:00
|
|
|
} else if self.text.contains(channel)
|
2026-06-23 15:42:28 +00:00
|
|
|
|| self
|
|
|
|
|
.text_prefixes
|
|
|
|
|
.iter()
|
2026-07-12 06:14:34 +00:00
|
|
|
.any(|prefix| channel.starts_with(prefix))
|
2026-06-23 15:42:28 +00:00
|
|
|
{
|
|
|
|
|
ChannelKind::Text
|
|
|
|
|
} else {
|
|
|
|
|
ChannelKind::Opaque
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChannelClassifier for ChannelRegistry {
|
2026-07-12 06:14:34 +00:00
|
|
|
fn classify(&self, channel_name: &str) -> ChannelKind {
|
|
|
|
|
self.classify_name(channel_name)
|
2026-06-23 15:42:28 +00:00
|
|
|
}
|
|
|
|
|
}
|