Skip to main content

onevcs/
vocabulary.rs

1//! The words this crate's event stream is written in: its source, its four
2//! phases, its reserved labels, and what a matcher may ask of them.
3//!
4//! `onemessagebus` knows the *shape* of an envelope — a version, a stamp, a
5//! stream, a sequence number, a source, a kind, the labels, the payload, the
6//! artifacts — and nothing about which words go in one. A [`Vocabulary`] supplies
7//! those words as types, and this is `onevcs`'s: the generic envelope shape is the
8//! bus core's, and every word in it is this crate's own.
9//!
10//! Declared here rather than taken from a shared profile crate. A vocabulary one
11//! program writes belongs to that program: `phase` is stamped on every event
12//! `onevcs` emits, [`PhaseOf`](crate::PhaseOf) classifies *this* crate's kinds into
13//! it, and [`EventStream`](crate::EventStream) works out which phases a session can
14//! produce and refuses a filter for one it cannot. A second library could not
15//! answer any of those, so it is not a second library's to declare.
16//!
17//! **The bytes do not move.** Every type here serializes to what `onevcs` has
18//! always written: `tests/recorded/onevcs-session.ndjson` is a stream a real
19//! publication left, and `tests/recorded.rs` reads every line of it back through
20//! these types and re-serializes it with no byte changed.
21//!
22//! [`Source`] is the core's *open* newtype rather than a closed enum. A filter is
23//! the one place a source word arrives from outside this crate, and a consumer
24//! merging several producers writes one filter for all of them: a word this crate
25//! does not produce is something it has no envelope for, which is an answer of
26//! none rather than a reason to refuse the question.
27
28use std::fmt;
29
30use onemessagebus::{Reserved, Vocabulary};
31use schemars::JsonSchema;
32use serde::{Deserialize, Serialize};
33use serde_json::{Map, Value};
34
35pub use onemessagebus::{ArtifactRef, Source};
36
37/// The word this crate stamps as the source of every event it writes.
38///
39/// Public because a consumer that merges this crate's stream with another's
40/// selects it by word — and a word each side spells its own copy of is a word
41/// they can come apart on.
42pub const SOURCE_WORD: &str = "vcs";
43
44/// The reserved label keys, in the order the wire lists them.
45pub const RESERVED_LABELS: &[Reserved] = &[
46    Reserved::text("run_id"),
47    Reserved::integer("round"),
48    Reserved::text("node"),
49    Reserved::text("step"),
50    Reserved::text("member"),
51    Reserved::text("persona"),
52];
53
54/// The reserved top-level dimensions: `phase` alone.
55pub const DIMENSIONS: &[Reserved] = &[Reserved::word("phase")];
56
57/// This crate's vocabulary: what [`Envelope`], [`EventFilter`] and
58/// [`EventMatcher`] are the bus core's generic types over.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct VcsEvents;
61
62impl Vocabulary for VcsEvents {
63    type Source = Source;
64    type Dimensions = Dimensions;
65    type Labels = Labels;
66    type Fields = MatchFields;
67
68    const NAME: &'static str = "vcs";
69    const RESERVED: &'static [Reserved] = RESERVED_LABELS;
70    const DIMENSIONS: &'static [Reserved] = DIMENSIONS;
71    const DEFAULT_SOURCE: &'static str = SOURCE_WORD;
72
73    /// One, for every source. This crate has written `v: 1` since the envelope
74    /// had a version, and the phase that arrived later is additive inside it — a
75    /// line written before there was one reads at the phase its kind decides.
76    fn write_version(_: &Self::Source) -> u32 {
77        1
78    }
79}
80
81/// One event of this crate: the bus core's envelope over this vocabulary.
82pub type Envelope = onemessagebus::Envelope<VcsEvents>;
83
84/// Which envelopes pass: the bus core's filter over this vocabulary.
85pub type EventFilter = onemessagebus::Filter<VcsEvents>;
86
87/// One matcher of an [`EventFilter`]: `source`, `kind`, and the [`MatchFields`] —
88/// `phase` and the five reserved labels a matcher may name.
89pub type EventMatcher = onemessagebus::Matcher<VcsEvents>;
90
91/// The bus core's emitter over this vocabulary.
92pub(crate) type Emitter = onemessagebus::Emitter<VcsEvents>;
93
94/// The bus core's reader over this vocabulary.
95pub(crate) type BusReader = onemessagebus::Reader<VcsEvents>;
96
97/// Which part of a change's life an event belongs to.
98///
99/// Four phases over one change: the work is made, it is brought together with the
100/// base it is going onto, it is proposed and ruled on, and what carries it is
101/// released. Stamped by the producer, never derived by a reader: one kind's phase
102/// is not always a fact about the kind.
103#[derive(
104    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
105)]
106#[serde(rename_all = "kebab-case")]
107pub enum Phase {
108    /// The work is being made.
109    Development,
110    /// The work is being brought together with the base.
111    Integrate,
112    /// The change request is open and being ruled on.
113    Review,
114    /// What carries the landed change is being released.
115    Release,
116}
117
118impl Phase {
119    /// The word this phase is spelled with, in a filter and in a rendering.
120    #[must_use]
121    pub const fn as_str(self) -> &'static str {
122        match self {
123            Phase::Development => "development",
124            Phase::Integrate => "integrate",
125            Phase::Review => "review",
126            Phase::Release => "release",
127        }
128    }
129
130    /// Every phase, in the order a refusal lists them.
131    #[must_use]
132    pub const fn every() -> [Phase; 4] {
133        [
134            Phase::Development,
135            Phase::Integrate,
136            Phase::Review,
137            Phase::Release,
138        ]
139    }
140}
141
142impl fmt::Display for Phase {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.write_str(self.as_str())
145    }
146}
147
148/// The envelope's reserved top-level dimensions: `phase`, optional and omitted
149/// from the wire when absent.
150///
151/// Carried between `kind` and `labels` on the wire. Refuses any other top-level
152/// key, which is what makes an envelope of this vocabulary reject an unknown field
153/// by name.
154#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
155#[serde(deny_unknown_fields)]
156pub struct Dimensions {
157    /// Which part of a change's life the event belongs to, as its producer
158    /// classified it.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub phase: Option<Phase>,
161}
162
163impl Dimensions {
164    /// No phase.
165    #[must_use]
166    pub const fn none() -> Self {
167        Self { phase: None }
168    }
169
170    /// At `phase`.
171    #[must_use]
172    pub const fn at(phase: Phase) -> Self {
173        Self { phase: Some(phase) }
174    }
175}
176
177impl From<Phase> for Dimensions {
178    fn from(phase: Phase) -> Self {
179        Self::at(phase)
180    }
181}
182
183impl From<Option<Phase>> for Dimensions {
184    fn from(phase: Option<Phase>) -> Self {
185        Self { phase }
186    }
187}
188
189/// The reserved label keys, plus whatever else a producer stamped.
190///
191/// Reserved keys are absent rather than empty when unknown, so an enricher can
192/// tell "not stamped" from "stamped empty". The extras are flattened beside them,
193/// in the order they were stamped — which is where this crate's own `session` and
194/// `identity` labels travel.
195#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
196pub struct Labels {
197    /// The run this event belongs to.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub run_id: Option<String>,
200    /// The round within the run.
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub round: Option<u64>,
203    /// The graph node being executed.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub node: Option<String>,
206    /// The step within a node that runs several in sequence.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub step: Option<String>,
209    /// Which member of a conversation produced the event.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub member: Option<String>,
212    /// The persona that member is running under.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub persona: Option<String>,
215    /// Free-form extras beyond the reserved keys above, carried untouched.
216    #[serde(flatten)]
217    pub extra: Map<String, Value>,
218}
219
220/// What a matcher may name beside `source` and `kind`: `phase` and the reserved
221/// labels, each by exact equality against what the envelope carries. `round` is
222/// deliberately not among them, as the grammar says.
223#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
224#[serde(deny_unknown_fields)]
225pub struct MatchFields {
226    /// The phase the envelope was stamped at.
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub phase: Option<Phase>,
229    /// The `run_id` label.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub run_id: Option<String>,
232    /// The `node` label.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub node: Option<String>,
235    /// The `step` label.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub step: Option<String>,
238    /// The `member` label.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub member: Option<String>,
241    /// The `persona` label.
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub persona: Option<String>,
244}