use serde::{Deserialize, Serialize};
use crate::{
FailureClass, FrameContext, IntegrationMode, LifecycleEventKind, PayloadEnvelope, PayloadRef,
ReceiptStatus, RetryClass, SCHEMA_VERSION, ValidationError, Warning, require_non_empty,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CallbackRequest {
pub schema_version: String,
pub event: LifecycleEventKind,
pub event_id: String,
pub adapter_id: String,
pub adapter_version: String,
pub integration_mode: IntegrationMode,
pub invocation_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness_session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness_run_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness_task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame_context: Option<FrameContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capability_snapshot_ref: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub payload_refs: Vec<PayloadRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sequence: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
pub metadata: serde_json::Map<String, serde_json::Value>,
}
impl CallbackRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema_version != SCHEMA_VERSION {
return Err(ValidationError::SchemaVersionMismatch {
expected: SCHEMA_VERSION.to_string(),
found: self.schema_version.clone(),
});
}
require_non_empty(&self.event_id, "request.event_id")?;
require_non_empty(&self.adapter_id, "request.adapter_id")?;
require_non_empty(&self.adapter_version, "request.adapter_version")?;
require_non_empty(&self.invocation_id, "request.invocation_id")?;
if let Some(s) = &self.harness_session_id {
require_non_empty(s, "request.harness_session_id")?;
}
if let Some(s) = &self.harness_run_id {
require_non_empty(s, "request.harness_run_id")?;
}
if let Some(s) = &self.harness_task_id {
require_non_empty(s, "request.harness_task_id")?;
}
if let Some(s) = &self.capability_snapshot_ref {
require_non_empty(s, "request.capability_snapshot_ref")?;
}
if let Some(s) = &self.idempotency_key {
require_non_empty(s, "request.idempotency_key")?;
}
if let Some(fc) = &self.frame_context {
fc.validate()?;
}
for r in &self.payload_refs {
r.validate()?;
}
match self.event {
LifecycleEventKind::FrameOpening
| LifecycleEventKind::FrameOpened
| LifecycleEventKind::FrameEnding
| LifecycleEventKind::FrameEnded
if self.frame_context.is_none() =>
{
Err(ValidationError::InvalidRequest(
"frame.* events require frame_context".into(),
))
}
LifecycleEventKind::ReceiptEmitted if self.idempotency_key.is_some() => {
Err(ValidationError::InvalidRequest(
"receipt.emitted is a notification event and must not carry an idempotency_key"
.into(),
))
}
_ => Ok(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CallbackResponse {
pub schema_version: String,
pub status: ReceiptStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub client_payloads: Vec<PayloadEnvelope>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receipt_refs: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<Warning>,
pub failure_class: Option<FailureClass>,
pub retry_class: Option<RetryClass>,
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
pub metadata: serde_json::Map<String, serde_json::Value>,
}
impl CallbackResponse {
pub fn ok(status: ReceiptStatus) -> Self {
Self {
schema_version: SCHEMA_VERSION.to_string(),
status,
client_payloads: Vec::new(),
receipt_refs: Vec::new(),
warnings: Vec::new(),
failure_class: None,
retry_class: None,
metadata: serde_json::Map::new(),
}
}
pub fn failed(failure: FailureClass) -> Self {
Self {
schema_version: SCHEMA_VERSION.to_string(),
status: ReceiptStatus::Failed,
client_payloads: Vec::new(),
receipt_refs: Vec::new(),
warnings: Vec::new(),
failure_class: Some(failure),
retry_class: Some(failure.default_retry()),
metadata: serde_json::Map::new(),
}
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema_version != SCHEMA_VERSION {
return Err(ValidationError::SchemaVersionMismatch {
expected: SCHEMA_VERSION.to_string(),
found: self.schema_version.clone(),
});
}
for p in &self.client_payloads {
p.validate()?;
}
for r in &self.receipt_refs {
require_non_empty(r, "response.receipt_refs[]")?;
}
for w in &self.warnings {
w.validate()?;
}
match (
matches!(self.status, ReceiptStatus::Failed),
self.failure_class.is_some(),
) {
(true, false) => {
return Err(ValidationError::InvalidResponse(
"status=failed requires failure_class".into(),
));
}
(false, true) => {
return Err(ValidationError::InvalidResponse(
"failure_class is only valid on status=failed responses".into(),
));
}
_ => {}
}
if matches!(self.status, ReceiptStatus::Failed) && self.retry_class.is_none() {
return Err(ValidationError::InvalidResponse(
"status=failed requires retry_class (clients must declare retry posture)".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DispatchEnvelope {
pub schema_version: String,
pub request: CallbackRequest,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub payloads: Vec<PayloadEnvelope>,
}
impl DispatchEnvelope {
pub fn new(request: CallbackRequest, payloads: Vec<PayloadEnvelope>) -> Self {
Self {
schema_version: SCHEMA_VERSION.to_string(),
request,
payloads,
}
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema_version != SCHEMA_VERSION {
return Err(ValidationError::SchemaVersionMismatch {
expected: SCHEMA_VERSION.to_string(),
found: self.schema_version.clone(),
});
}
self.request.validate()?;
for p in &self.payloads {
p.validate()?;
}
Ok(())
}
}