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}