1use std::any::TypeId;
10use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13use crate::persistence::{PersistenceDefinition, PersistenceKind};
14use crate::rpc::RegisteredRpc;
15use crate::step::RegisteredStep;
16use crate::step_options::ErasedStepOptions;
17use crate::{Context, Flow, HandlerResult, SdkError, SdkResult, StepDecision, Stream, Wait};
18use dex_protocol::dex::Value as ProtoValue;
19
20#[derive(Clone, Default)]
21pub struct Registry {
39 inner: Arc<RegistryInner>,
40}
41
42#[derive(Clone, Default)]
43struct RegistryInner {
44 flows: HashMap<&'static str, RegisteredFlow>,
45 attribute_indexes: HashMap<String, i32>,
46 stream_flows: HashMap<usize, &'static str>,
47}
48
49#[derive(Clone)]
50pub(crate) struct RegisteredFlow {
51 pub(crate) name: &'static str,
52 pub(crate) type_id: TypeId,
53 pub(crate) steps: HashMap<&'static str, RegisteredStep>,
54 pub(crate) start_step: Option<RegisteredStep>,
55 pub(crate) rpcs: HashMap<&'static str, RegisteredRpc>,
56 pub(crate) persistence: HashMap<String, PersistenceDefinition>,
57 pub(crate) handler: Arc<dyn ErasedFlow>,
58}
59
60impl Registry {
61 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn register<SomeFlow: Flow>(mut self, flow: SomeFlow) -> SdkResult<Self> {
73 let flow = Arc::new(flow);
74 let name = require_static_name(flow.flow_type(), "Flow type")?;
75 let registered = assemble_flow(name, Arc::clone(&flow))?;
76 let inner = Arc::make_mut(&mut self.inner);
77 let mut additions = Vec::new();
78 let mut stream_identities = Vec::new();
79 for definition in registered.persistence.values() {
80 if definition.kind == PersistenceKind::Stream {
81 let identity = definition.stream_identity.ok_or_else(|| {
82 definition_error(format!(
83 "Flow {name} Stream {} has no definition identity",
84 definition.name
85 ))
86 })?;
87 if inner.stream_flows.contains_key(&identity) {
88 return Err(definition_error(format!(
89 "Stream {} is registered by multiple Flows",
90 definition.name
91 )));
92 }
93 stream_identities.push(identity);
94 }
95 let Some(index) = &definition.index else {
96 continue;
97 };
98 if definition.kind == PersistenceKind::AttributeMap
99 && index.key().is_none_or(str::is_empty)
100 {
101 return Err(definition_error(format!(
102 "Flow {name} indexed AttributeMap {} requires an index key",
103 definition.name
104 )));
105 }
106 let key = index.key().unwrap_or(&definition.name).to_string();
107 let index_type = index.proto_value();
108 if inner
109 .attribute_indexes
110 .get(&key)
111 .is_some_and(|existing| *existing != index_type)
112 {
113 return Err(definition_error(format!(
114 "Attribute index {key} has conflicting types"
115 )));
116 }
117 additions.push((key, index_type));
118 }
119 if inner.flows.insert(name, registered).is_some() {
120 return Err(definition_error(format!("duplicate Flow {name}")));
121 }
122 inner.attribute_indexes.extend(additions);
123 for identity in stream_identities {
124 inner.stream_flows.insert(identity, name);
125 }
126 Ok(self)
127 }
128
129 pub(crate) fn flow(&self, name: &str) -> SdkResult<&RegisteredFlow> {
130 self.inner
131 .flows
132 .get(name)
133 .ok_or_else(|| SdkError::FlowDefinition {
134 message: format!("Flow is not registered: {name}"),
135 })
136 }
137
138 pub(crate) fn rpc(&self, name: &str) -> SdkResult<&RegisteredRpc> {
139 let mut matched = None;
140 for flow in self.inner.flows.values() {
141 let Some(rpc) = flow.rpcs.get(name) else {
142 continue;
143 };
144 if matched.is_some() {
145 return Err(SdkError::FlowDefinition {
146 message: format!("RPC name is ambiguous across registered Flows: {name}"),
147 });
148 }
149 matched = Some(rpc);
150 }
151 matched.ok_or_else(|| SdkError::FlowDefinition {
152 message: format!("RPC is not registered: {name}"),
153 })
154 }
155
156 pub(crate) fn flow_for_stream<T>(&self, stream: &Stream<T>) -> SdkResult<&RegisteredFlow> {
157 let flow_name = self
158 .inner
159 .stream_flows
160 .get(&stream.identity())
161 .ok_or_else(|| SdkError::FlowDefinition {
162 message: format!("Stream is not registered: {}", stream.name()),
163 })?;
164 self.flow(flow_name)
165 }
166
167 pub(crate) fn attribute_indexes(&self) -> &HashMap<String, i32> {
168 &self.inner.attribute_indexes
169 }
170}
171
172fn assemble_flow<SomeFlow: Flow>(
173 name: &'static str,
174 flow: Arc<SomeFlow>,
175) -> SdkResult<RegisteredFlow> {
176 let timeout_handler = flow.timeout_handler();
177 let mut steps = HashMap::new();
178 let mut start_step = None;
179 for step in flow.steps().into_definitions() {
180 require_static_name(step.name, "Step type")?;
181 if step.starting {
182 if start_step.is_some() {
183 return Err(definition_error(format!(
184 "Flow {name} must not have multiple start Steps"
185 )));
186 }
187 start_step = Some(step.clone());
188 }
189 if steps.insert(step.name, step).is_some() {
190 return Err(definition_error(format!(
191 "Flow {name} has a duplicate Step"
192 )));
193 }
194 }
195
196 let persistence = assemble_persistence(name, flow.persistence().definitions())?;
197 let mut rpcs = HashMap::new();
198 for rpc in flow.rpcs().bind(Arc::clone(&flow)) {
199 require_static_name(rpc.name, "RPC name")?;
200 validate_rpc(name, &rpc, &persistence)?;
201 if rpcs.insert(rpc.name, rpc).is_some() {
202 return Err(definition_error(format!("Flow {name} has a duplicate RPC")));
203 }
204 }
205 Ok(RegisteredFlow {
206 name,
207 type_id: TypeId::of::<SomeFlow>(),
208 steps,
209 start_step,
210 rpcs,
211 persistence,
212 handler: Arc::new(TypedFlow {
213 flow,
214 timeout_handler,
215 }),
216 })
217}
218
219pub(crate) trait ErasedFlow: Send + Sync {
220 fn has_timeout_handler(&self) -> bool;
221 fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision>;
222 fn wait_for(
223 &self,
224 step_type: &str,
225 context: &mut Context,
226 input: &ProtoValue,
227 ) -> HandlerResult<Wait>;
228 fn execute(
229 &self,
230 step_type: &str,
231 context: &mut Context,
232 input: &ProtoValue,
233 ) -> HandlerResult<StepDecision>;
234 fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions>;
235}
236
237struct TypedFlow<SomeFlow> {
238 flow: Arc<SomeFlow>,
239 timeout_handler: Option<crate::FlowTimeoutHandler<SomeFlow>>,
240}
241
242impl<SomeFlow: Flow> ErasedFlow for TypedFlow<SomeFlow> {
243 fn has_timeout_handler(&self) -> bool {
244 self.timeout_handler.is_some()
245 }
246
247 fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision> {
248 let handler = self.timeout_handler.ok_or_else(|| {
249 crate::HandlerError::new("dex_sdk::HandlerError", "Flow has no timeout handler")
250 })?;
251 handler(self.flow.as_ref(), context)
252 }
253 fn wait_for(
254 &self,
255 step_type: &str,
256 context: &mut Context,
257 input: &ProtoValue,
258 ) -> HandlerResult<Wait> {
259 self.flow
260 .steps()
261 .find(step_type)
262 .ok_or_else(|| {
263 crate::HandlerError::new(
264 "dex_sdk::HandlerError",
265 format!("Step is not registered: {step_type}"),
266 )
267 })?
268 .wait_for(context, input)
269 }
270
271 fn execute(
272 &self,
273 step_type: &str,
274 context: &mut Context,
275 input: &ProtoValue,
276 ) -> HandlerResult<StepDecision> {
277 self.flow
278 .steps()
279 .find(step_type)
280 .ok_or_else(|| {
281 crate::HandlerError::new(
282 "dex_sdk::HandlerError",
283 format!("Step is not registered: {step_type}"),
284 )
285 })?
286 .execute(context, input)
287 }
288
289 fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions> {
290 Ok(self
291 .flow
292 .steps()
293 .find(step_type)
294 .ok_or_else(|| {
295 crate::HandlerError::new(
296 "dex_sdk::HandlerError",
297 format!("Step is not registered: {step_type}"),
298 )
299 })?
300 .options())
301 }
302}
303
304fn assemble_persistence(
305 flow_name: &str,
306 definitions: &[PersistenceDefinition],
307) -> SdkResult<HashMap<String, PersistenceDefinition>> {
308 let mut persistence = HashMap::new();
309 for definition in definitions {
310 if definition.name.is_empty() {
311 return Err(definition_error(format!(
312 "Flow {flow_name} has an empty persistence definition name"
313 )));
314 }
315 if matches!(
316 definition.kind,
317 PersistenceKind::Attribute
318 | PersistenceKind::AttributeMap
319 | PersistenceKind::Channel
320 | PersistenceKind::ChannelMap
321 ) && definition.name.contains('/')
322 {
323 return Err(definition_error(format!(
324 "Flow {flow_name} persistence definition {} must not contain /",
325 definition.name
326 )));
327 }
328 if persistence
329 .insert(definition.name.clone(), definition.clone())
330 .is_some()
331 {
332 return Err(definition_error(format!(
333 "Flow {flow_name} has duplicate persistence definition {}",
334 definition.name
335 )));
336 }
337 }
338 Ok(persistence)
339}
340
341fn validate_rpc(
342 flow_name: &str,
343 rpc: &RegisteredRpc,
344 persistence: &HashMap<String, PersistenceDefinition>,
345) -> SdkResult<()> {
346 if rpc.timeout.is_some_and(|timeout| timeout.is_zero()) {
347 return Err(definition_error(format!(
348 "Flow {flow_name} RPC {} timeout must be positive",
349 rpc.name
350 )));
351 }
352 let mut locks = HashSet::new();
353 for lock in &rpc.locks {
354 let physical_name = lock.physical_name();
355 let logical_name = physical_name.split('/').next().unwrap_or(&physical_name);
356 let definition = persistence.get(logical_name).ok_or_else(|| {
357 definition_error(format!(
358 "Flow {flow_name} RPC {} locks an unregistered Attribute",
359 rpc.name
360 ))
361 })?;
362 if !matches!(
363 definition.kind,
364 PersistenceKind::Attribute | PersistenceKind::AttributeMap
365 ) {
366 return Err(definition_error(format!(
367 "Flow {flow_name} RPC {} locks a non-Attribute",
368 rpc.name
369 )));
370 }
371 if !locks.insert(physical_name) {
372 return Err(definition_error(format!(
373 "Flow {flow_name} RPC {} has a duplicate lock",
374 rpc.name
375 )));
376 }
377 }
378 Ok(())
379}
380
381fn require_static_name(name: &'static str, kind: &str) -> SdkResult<&'static str> {
382 if name.is_empty() {
383 Err(definition_error(format!("{kind} is required")))
384 } else {
385 Ok(name)
386 }
387}
388
389fn definition_error(message: impl Into<String>) -> SdkError {
390 SdkError::FlowDefinition {
391 message: message.into(),
392 }
393}
394
395pub(crate) fn physical_name(name: &str, instance: &str) -> String {
396 if instance.is_empty() {
397 panic!("persistence instance is required");
398 }
399 let mut encoded = String::new();
400 for byte in instance.bytes() {
401 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
402 encoded.push(char::from(byte));
403 } else {
404 encoded.push_str(&format!("%{byte:02X}"));
405 }
406 }
407 format!("{name}/{encoded}")
408}
409
410pub(crate) fn decode_instance(physical_name: &str, prefix: &str) -> Result<String, String> {
411 let encoded = physical_name
412 .strip_prefix(prefix)
413 .ok_or_else(|| format!("map key {physical_name} does not start with {prefix}"))?;
414 let bytes = encoded.as_bytes();
415 let mut decoded = Vec::with_capacity(bytes.len());
416 let mut index = 0;
417 while index < bytes.len() {
418 if bytes[index] == b'%' {
419 if index + 2 >= bytes.len() {
420 return Err(format!("invalid percent encoding in {physical_name}"));
421 }
422 let high = hex_value(bytes[index + 1])?;
423 let low = hex_value(bytes[index + 2])?;
424 decoded.push(high * 16 + low);
425 index += 3;
426 } else {
427 decoded.push(bytes[index]);
428 index += 1;
429 }
430 }
431 String::from_utf8(decoded).map_err(|error| error.to_string())
432}
433
434fn hex_value(byte: u8) -> Result<u8, String> {
435 match byte {
436 b'0'..=b'9' => Ok(byte - b'0'),
437 b'a'..=b'f' => Ok(byte - b'a' + 10),
438 b'A'..=b'F' => Ok(byte - b'A' + 10),
439 _ => Err(format!(
440 "invalid percent-encoding digit {}",
441 char::from(byte)
442 )),
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::{PersistenceDefinition, PersistenceKind, assemble_persistence, physical_name};
449
450 #[test]
451 fn persistence_definition_names_reserve_slash() {
452 for kind in [
453 PersistenceKind::Attribute,
454 PersistenceKind::AttributeMap,
455 PersistenceKind::Channel,
456 PersistenceKind::ChannelMap,
457 ] {
458 let result = assemble_persistence(
459 "TestFlow",
460 &[PersistenceDefinition {
461 name: "orders/by-id".to_string(),
462 kind,
463 index: None,
464 sync_to_attribute_store: false,
465 stream_identity: None,
466 stream_capacity_bytes: None,
467 }],
468 );
469 assert!(result.is_err());
470 }
471
472 assert_eq!(
473 physical_name("messages", "orders/by-id"),
474 "messages/orders%2Fby-id"
475 );
476 }
477}