use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
use crate::error::{self, Result};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Envelope {
pub v: u32,
pub ts: String,
pub stream: String,
pub seq: u64,
pub source: Source,
pub kind: EventKind,
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,
GateStarted,
GateVerdict,
CommitPreserved,
Push,
ChangeOpened,
ChangeCheck,
ChangeMerged,
MergeQueued,
MergeCompleted,
RecoveryAttested,
SyncConflict,
SessionClosed,
}
#[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::GateStarted => "gate-started",
EventKind::GateVerdict => "gate-verdict",
EventKind::CommitPreserved => "commit-preserved",
EventKind::Push => "push",
EventKind::ChangeOpened => "change-opened",
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",
}
}
}
const MATCHER_FIELDS: &str = "source, 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 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
.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}"))?,
);
}
"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",
}
}