use crate::{
AdapterManifest, CallbackRequest, FrameContext, IntegrationMode, LifecycleEventKind,
PayloadRef, SCHEMA_VERSION,
};
use super::validation::{AdapterRegistry, AdapterResolution, RouteError, manifest_of};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutingPlan {
pub event: LifecycleEventKind,
pub event_id: String,
pub invocation_id: String,
pub adapter: AdapterManifest,
pub integration_mode: IntegrationMode,
pub frame_context: Option<FrameContext>,
pub payload_refs: Vec<PayloadRef>,
pub capability_snapshot_ref: Option<String>,
pub sequence: Option<u64>,
pub idempotency_key: Option<String>,
}
pub fn route<R: AdapterRegistry>(
req: &CallbackRequest,
registry: &R,
) -> Result<RoutingPlan, RouteError> {
if req.schema_version != SCHEMA_VERSION {
return Err(RouteError::SchemaVersionMismatch {
expected: SCHEMA_VERSION.to_string(),
found: req.schema_version.clone(),
});
}
require_non_empty(&req.event_id, "request.event_id")?;
require_non_empty(&req.adapter_id, "request.adapter_id")?;
require_non_empty(&req.adapter_version, "request.adapter_version")?;
require_non_empty(&req.invocation_id, "request.invocation_id")?;
if let Some(s) = &req.harness_session_id {
require_non_empty(s, "request.harness_session_id")?;
}
if let Some(s) = &req.harness_run_id {
require_non_empty(s, "request.harness_run_id")?;
}
if let Some(s) = &req.harness_task_id {
require_non_empty(s, "request.harness_task_id")?;
}
if let Some(s) = &req.capability_snapshot_ref {
require_non_empty(s, "request.capability_snapshot_ref")?;
}
if let Some(s) = &req.idempotency_key {
require_non_empty(s, "request.idempotency_key")?;
}
if let Some(fc) = &req.frame_context {
validate_frame_context(fc)?;
}
require_frame_context_for_event(req)?;
if matches!(req.event, LifecycleEventKind::ReceiptEmitted) && req.idempotency_key.is_some() {
return Err(RouteError::InvalidEventEnvelope {
detail: "receipt.emitted is a notification event and must not carry \
an idempotency_key"
.into(),
});
}
for (idx, r) in req.payload_refs.iter().enumerate() {
if r.payload_id.is_empty() {
return Err(RouteError::InvalidPayloadRef {
index: idx,
detail: "payload_ref.payload_id is empty".into(),
});
}
if r.payload_kind.is_empty() {
return Err(RouteError::InvalidPayloadRef {
index: idx,
detail: "payload_ref.payload_kind is empty".into(),
});
}
}
let resolution = registry.resolve(&req.adapter_id, &req.adapter_version);
let manifest = match &resolution {
AdapterResolution::Found(_) => manifest_of(&resolution).expect("Found carries manifest"),
AdapterResolution::UnknownId => {
return Err(RouteError::AdapterIdNotFound {
adapter_id: req.adapter_id.clone(),
});
}
AdapterResolution::VersionMismatch { registered_version } => {
return Err(RouteError::AdapterVersionMismatch {
adapter_id: req.adapter_id.clone(),
requested: req.adapter_version.clone(),
registered: registered_version.clone(),
});
}
};
Ok(RoutingPlan {
event: req.event,
event_id: req.event_id.clone(),
invocation_id: req.invocation_id.clone(),
adapter: manifest.clone(),
integration_mode: req.integration_mode,
frame_context: req.frame_context.clone(),
payload_refs: req.payload_refs.clone(),
capability_snapshot_ref: req.capability_snapshot_ref.clone(),
sequence: req.sequence,
idempotency_key: req.idempotency_key.clone(),
})
}
fn require_non_empty(value: &str, field: &'static str) -> Result<(), RouteError> {
if value.is_empty() {
Err(RouteError::EmptySentinel { field })
} else {
Ok(())
}
}
fn validate_frame_context(fc: &FrameContext) -> Result<(), RouteError> {
if fc.frame_id.is_empty() {
return Err(RouteError::InvalidFrameContext {
detail: "frame_id is empty".into(),
});
}
if let Some(parent) = &fc.parent_frame_id
&& parent.is_empty()
{
return Err(RouteError::InvalidFrameContext {
detail: "parent_frame_id is empty".into(),
});
}
match (fc.frame_class, &fc.parent_frame_id) {
(crate::FrameClass::TopLevel, Some(_)) => Err(RouteError::InvalidFrameContext {
detail: "frame_class=top_level must not carry parent_frame_id".into(),
}),
(crate::FrameClass::Subcall, None) => Err(RouteError::InvalidFrameContext {
detail: "frame_class=subcall requires parent_frame_id".into(),
}),
_ => Ok(()),
}
}
fn require_frame_context_for_event(req: &CallbackRequest) -> Result<(), RouteError> {
let needs_frame = matches!(
req.event,
LifecycleEventKind::FrameOpening
| LifecycleEventKind::FrameOpened
| LifecycleEventKind::FrameEnding
| LifecycleEventKind::FrameEnded
);
if needs_frame && req.frame_context.is_none() {
return Err(RouteError::InvalidFrameContext {
detail: "frame.* events require frame_context".into(),
});
}
Ok(())
}