use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
use crate::error::{self, Result};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "StoredEnvelope")]
pub struct Envelope {
pub v: u32,
pub ts: String,
pub stream: String,
pub seq: u64,
pub source: Source,
pub kind: EventKind,
pub phase: Phase,
pub labels: Labels,
pub payload: Map<String, Value>,
pub artifacts: Vec<ArtifactRef>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Source {
Agentgraph,
Vcs,
Pipeline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EventKind {
SessionOpened,
Fetch,
LockWait,
LockAcquired,
CommitPreserved,
Push,
ChangeOpened,
ChangeDrafted,
DraftLifted,
ChangeCheck,
ChangeMerged,
MergeQueued,
MergeCompleted,
RecoveryAttested,
SyncConflict,
SessionClosed,
ReleaseProbed,
ReleaseAcknowledged,
ReleaseObserved,
}
#[derive(Deserialize)]
struct StoredEnvelope {
v: u32,
ts: String,
stream: String,
seq: u64,
source: Source,
kind: StoredKind,
#[serde(default)]
phase: Option<Phase>,
labels: Labels,
payload: Map<String, Value>,
artifacts: Vec<ArtifactRef>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum StoredKind {
Known(EventKind),
Unknown(String),
}
impl StoredEnvelope {
fn read(self) -> std::result::Result<Envelope, String> {
let kind = match self.kind {
StoredKind::Known(kind) => kind,
StoredKind::Unknown(spelling) => return Err(spelling),
};
Ok(Envelope {
v: self.v,
ts: self.ts,
stream: self.stream,
seq: self.seq,
source: self.source,
kind,
phase: self
.phase
.or_else(|| Phase::of(kind))
.unwrap_or(Phase::Development),
labels: self.labels,
payload: self.payload,
artifacts: self.artifacts,
})
}
}
impl TryFrom<StoredEnvelope> for Envelope {
type Error = String;
fn try_from(stored: StoredEnvelope) -> std::result::Result<Self, Self::Error> {
stored
.read()
.map_err(|kind| format!("unknown variant {kind:?} for an event kind"))
}
}
pub(crate) enum Line {
Known(Box<Envelope>),
Unknown(UnknownKind),
}
pub(crate) struct UnknownKind {
pub v: u32,
pub ts: String,
pub stream: String,
}
impl Line {
pub(crate) fn read(line: &str) -> serde_json::Result<Self> {
let stored: StoredEnvelope = serde_json::from_str(line)?;
let header = UnknownKind {
v: stored.v,
ts: stored.ts.clone(),
stream: stored.stream.clone(),
};
Ok(match stored.read() {
Ok(envelope) => Line::Known(Box::new(envelope)),
Err(_) => Line::Unknown(header),
})
}
pub(crate) fn stream(&self) -> &str {
match self {
Line::Known(envelope) => &envelope.stream,
Line::Unknown(header) => &header.stream,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Phase {
Development,
Integrate,
Review,
Release,
}
impl Phase {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Phase::Development => "development",
Phase::Integrate => "integrate",
Phase::Review => "review",
Phase::Release => "release",
}
}
#[must_use]
pub fn every() -> [Phase; 4] {
[
Phase::Development,
Phase::Integrate,
Phase::Review,
Phase::Release,
]
}
#[must_use]
pub fn of(kind: EventKind) -> Option<Phase> {
Some(match kind {
EventKind::SessionOpened
| EventKind::Fetch
| EventKind::LockWait
| EventKind::LockAcquired
| EventKind::CommitPreserved
| EventKind::RecoveryAttested
| EventKind::SessionClosed => Phase::Development,
EventKind::MergeQueued | EventKind::MergeCompleted | EventKind::SyncConflict => {
Phase::Integrate
}
EventKind::ChangeOpened
| EventKind::ChangeDrafted
| EventKind::DraftLifted
| EventKind::ChangeCheck
| EventKind::ChangeMerged => Phase::Review,
EventKind::ReleaseProbed
| EventKind::ReleaseAcknowledged
| EventKind::ReleaseObserved => Phase::Release,
EventKind::Push => return None,
})
}
}
impl std::fmt::Display for Phase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Labels {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub round: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub member: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persona: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactRef {
pub id: ArtifactId,
pub kind: String,
pub bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ArtifactId(pub String);
impl EventKind {
fn wire(self) -> &'static str {
match self {
EventKind::SessionOpened => "session-opened",
EventKind::Fetch => "fetch",
EventKind::LockWait => "lock-wait",
EventKind::LockAcquired => "lock-acquired",
EventKind::CommitPreserved => "commit-preserved",
EventKind::Push => "push",
EventKind::ChangeOpened => "change-opened",
EventKind::ChangeDrafted => "change-drafted",
EventKind::DraftLifted => "draft-lifted",
EventKind::ChangeCheck => "change-check",
EventKind::ChangeMerged => "change-merged",
EventKind::MergeQueued => "merge-queued",
EventKind::MergeCompleted => "merge-completed",
EventKind::RecoveryAttested => "recovery-attested",
EventKind::SyncConflict => "sync-conflict",
EventKind::SessionClosed => "session-closed",
EventKind::ReleaseProbed => "release-probed",
EventKind::ReleaseAcknowledged => "release-acknowledged",
EventKind::ReleaseObserved => "release-observed",
}
}
}
const MATCHER_FIELDS: &str = "source, phase, kind, run_id, node, step, member, persona";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct EventFilter {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub include: Vec<EventMatcher>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub exclude: Vec<EventMatcher>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct EventMatcher {
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Source>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<Phase>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub member: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub persona: Option<String>,
}
impl EventFilter {
pub fn parse(spec: &str) -> Result<Self> {
serde_yaml_ng::from_str(spec)
.map_err(|failure| error::invalid(format!("the event filter is unusable: {failure}")))
}
#[must_use]
pub fn matches(&self, envelope: &Envelope) -> bool {
if self.exclude.iter().any(|matcher| matcher.matches(envelope)) {
return false;
}
self.include.is_empty() || self.include.iter().any(|matcher| matcher.matches(envelope))
}
fn from_document(document: &Value) -> std::result::Result<Self, String> {
let object = document.as_object().ok_or_else(|| {
format!(
"an event filter is a mapping of `include` and `exclude`, not {}",
shape(document)
)
})?;
if let Some(stray) = object
.keys()
.find(|key| !matches!(key.as_str(), "include" | "exclude"))
{
return Err(format!(
"an event filter names `include` and `exclude`; {stray:?} is neither"
));
}
Ok(Self {
include: matchers(object.get("include"), "include")?,
exclude: matchers(object.get("exclude"), "exclude")?,
})
}
}
impl<'de> Deserialize<'de> for EventFilter {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
let document = Value::deserialize(deserializer)?;
Self::from_document(&document).map_err(serde::de::Error::custom)
}
}
impl EventMatcher {
fn matches(&self, envelope: &Envelope) -> bool {
let labels = &envelope.labels;
self.source.is_none_or(|want| want == envelope.source)
&& self.phase.is_none_or(|want| want == envelope.phase)
&& self
.kind
.as_ref()
.is_none_or(|pattern| crate::policy::glob(pattern, envelope.kind.wire()))
&& stamped(&self.run_id, &labels.run_id)
&& stamped(&self.node, &labels.node)
&& stamped(&self.step, &labels.step)
&& stamped(&self.member, &labels.member)
&& stamped(&self.persona, &labels.persona)
}
}
fn stamped(want: &Option<String>, stamped: &Option<String>) -> bool {
match want {
None => true,
Some(want) => stamped.as_deref() == Some(want.as_str()),
}
}
fn matchers(value: Option<&Value>, list: &str) -> std::result::Result<Vec<EventMatcher>, String> {
let Some(value) = value else {
return Ok(Vec::new());
};
let entries = value.as_array().ok_or_else(|| {
format!(
"an event filter's `{list}` is a list of matchers, not {}",
shape(value)
)
})?;
entries
.iter()
.enumerate()
.map(|(index, entry)| matcher(entry, list, index + 1))
.collect()
}
fn matcher(
value: &Value,
list: &str,
position: usize,
) -> std::result::Result<EventMatcher, String> {
let named = format!("the event filter's {list} matcher {position}");
let fields = value.as_object().ok_or_else(|| {
format!(
"{named} is a mapping of matcher fields, not {}",
shape(value)
)
})?;
let mut matcher = EventMatcher::default();
for (field, value) in fields {
match field.as_str() {
"source" => {
matcher.source = Some(
serde_json::from_value(value.clone())
.map_err(|failure| format!("{named} names no source family: {failure}"))?,
);
}
"phase" => {
matcher.phase = Some(
serde_json::from_value(value.clone())
.map_err(|failure| format!("{named} names no phase: {failure}"))?,
);
}
"kind" => matcher.kind = Some(text(value, &named, field)?),
"run_id" => matcher.run_id = Some(text(value, &named, field)?),
"node" => matcher.node = Some(text(value, &named, field)?),
"step" => matcher.step = Some(text(value, &named, field)?),
"member" => matcher.member = Some(text(value, &named, field)?),
"persona" => matcher.persona = Some(text(value, &named, field)?),
unknown => {
return Err(format!(
"{named} names {unknown:?}, which is not a matcher field ({MATCHER_FIELDS})"
))
}
}
}
Ok(matcher)
}
fn text(value: &Value, named: &str, field: &str) -> std::result::Result<String, String> {
value.as_str().map(str::to_owned).ok_or_else(|| {
format!(
"{named} matches {field} against {}, which is not a string",
shape(value)
)
})
}
fn shape(value: &Value) -> &'static str {
match value {
Value::Null => "nothing",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "a list",
Value::Object(_) => "a mapping",
}
}