1use crate::{
12 AdapterManifest, CallbackRequest, FrameContext, IntegrationMode, LifecycleEventKind,
13 PayloadRef, SCHEMA_VERSION,
14};
15
16use super::validation::{AdapterRegistry, AdapterResolution, RouteError, manifest_of};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct RoutingPlan {
31 pub event: LifecycleEventKind,
33 pub event_id: String,
35 pub invocation_id: String,
37 pub adapter: AdapterManifest,
40 pub integration_mode: IntegrationMode,
45 pub frame_context: Option<FrameContext>,
47 pub payload_refs: Vec<PayloadRef>,
50 pub capability_snapshot_ref: Option<String>,
52 pub sequence: Option<u64>,
54 pub idempotency_key: Option<String>,
56}
57
58pub fn route<R: AdapterRegistry>(
71 req: &CallbackRequest,
72 registry: &R,
73) -> Result<RoutingPlan, RouteError> {
74 if req.schema_version != SCHEMA_VERSION {
76 return Err(RouteError::SchemaVersionMismatch {
77 expected: SCHEMA_VERSION.to_string(),
78 found: req.schema_version.clone(),
79 });
80 }
81
82 require_non_empty(&req.event_id, "request.event_id")?;
87 require_non_empty(&req.adapter_id, "request.adapter_id")?;
88 require_non_empty(&req.adapter_version, "request.adapter_version")?;
89 require_non_empty(&req.invocation_id, "request.invocation_id")?;
90 if let Some(s) = &req.harness_session_id {
91 require_non_empty(s, "request.harness_session_id")?;
92 }
93 if let Some(s) = &req.harness_run_id {
94 require_non_empty(s, "request.harness_run_id")?;
95 }
96 if let Some(s) = &req.harness_task_id {
97 require_non_empty(s, "request.harness_task_id")?;
98 }
99 if let Some(s) = &req.capability_snapshot_ref {
100 require_non_empty(s, "request.capability_snapshot_ref")?;
101 }
102 if let Some(s) = &req.idempotency_key {
103 require_non_empty(s, "request.idempotency_key")?;
104 }
105
106 if let Some(fc) = &req.frame_context {
108 validate_frame_context(fc)?;
109 }
110 require_frame_context_for_event(req)?;
111
112 if matches!(req.event, LifecycleEventKind::ReceiptEmitted) && req.idempotency_key.is_some() {
114 return Err(RouteError::InvalidEventEnvelope {
115 detail: "receipt.emitted is a notification event and must not carry \
116 an idempotency_key"
117 .into(),
118 });
119 }
120
121 for (idx, r) in req.payload_refs.iter().enumerate() {
123 if r.payload_id.is_empty() {
124 return Err(RouteError::InvalidPayloadRef {
125 index: idx,
126 detail: "payload_ref.payload_id is empty".into(),
127 });
128 }
129 if r.payload_kind.is_empty() {
130 return Err(RouteError::InvalidPayloadRef {
131 index: idx,
132 detail: "payload_ref.payload_kind is empty".into(),
133 });
134 }
135 }
136
137 let resolution = registry.resolve(&req.adapter_id, &req.adapter_version);
139 let manifest = match &resolution {
140 AdapterResolution::Found(_) => manifest_of(&resolution).expect("Found carries manifest"),
141 AdapterResolution::UnknownId => {
142 return Err(RouteError::AdapterIdNotFound {
143 adapter_id: req.adapter_id.clone(),
144 });
145 }
146 AdapterResolution::VersionMismatch { registered_version } => {
147 return Err(RouteError::AdapterVersionMismatch {
148 adapter_id: req.adapter_id.clone(),
149 requested: req.adapter_version.clone(),
150 registered: registered_version.clone(),
151 });
152 }
153 };
154
155 Ok(RoutingPlan {
156 event: req.event,
157 event_id: req.event_id.clone(),
158 invocation_id: req.invocation_id.clone(),
159 adapter: manifest.clone(),
160 integration_mode: req.integration_mode,
161 frame_context: req.frame_context.clone(),
162 payload_refs: req.payload_refs.clone(),
163 capability_snapshot_ref: req.capability_snapshot_ref.clone(),
164 sequence: req.sequence,
165 idempotency_key: req.idempotency_key.clone(),
166 })
167}
168
169fn require_non_empty(value: &str, field: &'static str) -> Result<(), RouteError> {
170 if value.is_empty() {
171 Err(RouteError::EmptySentinel { field })
172 } else {
173 Ok(())
174 }
175}
176
177fn validate_frame_context(fc: &FrameContext) -> Result<(), RouteError> {
192 if fc.frame_id.is_empty() {
193 return Err(RouteError::InvalidFrameContext {
194 detail: "frame_id is empty".into(),
195 });
196 }
197 if let Some(parent) = &fc.parent_frame_id
198 && parent.is_empty()
199 {
200 return Err(RouteError::InvalidFrameContext {
201 detail: "parent_frame_id is empty".into(),
202 });
203 }
204 match (fc.frame_class, &fc.parent_frame_id) {
205 (crate::FrameClass::TopLevel, Some(_)) => Err(RouteError::InvalidFrameContext {
206 detail: "frame_class=top_level must not carry parent_frame_id".into(),
207 }),
208 (crate::FrameClass::Subcall, None) => Err(RouteError::InvalidFrameContext {
209 detail: "frame_class=subcall requires parent_frame_id".into(),
210 }),
211 _ => Ok(()),
212 }
213}
214
215fn require_frame_context_for_event(req: &CallbackRequest) -> Result<(), RouteError> {
216 let needs_frame = matches!(
217 req.event,
218 LifecycleEventKind::FrameOpening
219 | LifecycleEventKind::FrameOpened
220 | LifecycleEventKind::FrameEnding
221 | LifecycleEventKind::FrameEnded
222 );
223 if needs_frame && req.frame_context.is_none() {
224 return Err(RouteError::InvalidFrameContext {
225 detail: "frame.* events require frame_context".into(),
226 });
227 }
228 Ok(())
229}