use async_trait::async_trait;
use serde::Deserialize;
use super::{ProviderContext, ProviderReport, ReleaseProvider};
use crate::runner::RunnerError;
pub const EVENT_LOG_PROVIDER: &str = "event-log";
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct EventLogConfig {
#[serde(default = "default_provider")]
pub provider: String,
pub bucket: String,
#[serde(default)]
pub prefix: Option<String>,
}
fn default_provider() -> String {
"r2".to_string()
}
impl EventLogConfig {
pub fn from_with(with: &serde_json::Value) -> Result<Self, RunnerError> {
serde_json::from_value(with.clone()).map_err(|e| {
RunnerError::InvalidConfig(format!(
"event-log outcome `with` is malformed: {e} (need at least `bucket = \"...\"`)"
))
})
}
pub fn dest(&self) -> String {
match &self.prefix {
Some(p) if !p.is_empty() => format!("{}/{p}", self.bucket),
_ => self.bucket.clone(),
}
}
}
#[derive(Debug, Default)]
pub struct EventLogProvider;
#[async_trait]
impl ReleaseProvider for EventLogProvider {
fn name(&self) -> &str {
EVENT_LOG_PROVIDER
}
async fn dispatch(&self, ctx: &ProviderContext<'_>) -> Result<ProviderReport, RunnerError> {
let cfg = EventLogConfig::from_with(ctx.config)?;
Ok(ProviderReport::action(format!(
"event log spools to {}://{}/<run_id>.events.jsonl on completion (uploaded by the daemon that owns the log)",
cfg.provider,
cfg.dest(),
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::MapSecrets;
use serde_json::json;
#[test]
fn parses_minimal_config_defaulting_provider_to_r2() {
let cfg = EventLogConfig::from_with(&json!({ "bucket": "yah-qed-logs" })).unwrap();
assert_eq!(cfg.provider, "r2");
assert_eq!(cfg.bucket, "yah-qed-logs");
assert_eq!(cfg.prefix, None);
assert_eq!(cfg.dest(), "yah-qed-logs");
}
#[test]
fn parses_full_config_and_builds_prefixed_dest() {
let cfg = EventLogConfig::from_with(
&json!({ "provider": "pond", "bucket": "logs", "prefix": "qed/events" }),
)
.unwrap();
assert_eq!(cfg.provider, "pond");
assert_eq!(cfg.dest(), "logs/qed/events");
}
#[test]
fn missing_bucket_is_typed_config_error() {
let err = EventLogConfig::from_with(&json!({ "provider": "r2" })).unwrap_err();
assert!(format!("{err}").contains("bucket"), "names the field: {err}");
}
#[tokio::test]
async fn dispatch_declares_intent_without_uploading() {
let provider = EventLogProvider::default();
let work = tempfile::tempdir().unwrap();
let cfg = json!({ "bucket": "logs", "prefix": "qed" });
let secrets = MapSecrets::default();
let ctx = ProviderContext {
version: "1.2.3",
artifacts: &[],
base_url: None,
config: &cfg,
work_dir: work.path(),
secrets: &secrets,
dry_run: false,
};
let report = provider.dispatch(&ctx).await.unwrap();
assert_eq!(report.actions.len(), 1);
assert!(report.actions[0].contains("logs/qed"));
assert!(report.published.is_empty(), "the adapter ships nothing itself");
assert!(report.produced.is_empty());
}
}