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 for step in steps.values() {
198 let options = flow
199 .steps()
200 .find(step.name)
201 .ok_or_else(|| definition_error(format!("Flow {name} Step disappeared")))?
202 .options();
203 validate_step_state_loads(name, step.name, &options, &persistence)?;
204 }
205 let mut rpcs = HashMap::new();
206 for rpc in flow.rpcs().bind(Arc::clone(&flow)) {
207 require_static_name(rpc.name, "RPC name")?;
208 validate_rpc(name, &rpc, &persistence)?;
209 if rpcs.insert(rpc.name, rpc).is_some() {
210 return Err(definition_error(format!("Flow {name} has a duplicate RPC")));
211 }
212 }
213 Ok(RegisteredFlow {
214 name,
215 type_id: TypeId::of::<SomeFlow>(),
216 steps,
217 start_step,
218 rpcs,
219 persistence,
220 handler: Arc::new(TypedFlow {
221 flow,
222 timeout_handler,
223 }),
224 })
225}
226
227pub(crate) trait ErasedFlow: Send + Sync {
228 fn has_timeout_handler(&self) -> bool;
229 fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision>;
230 fn wait_for(
231 &self,
232 step_type: &str,
233 context: &mut Context,
234 input: &ProtoValue,
235 ) -> HandlerResult<Wait>;
236 fn execute(
237 &self,
238 step_type: &str,
239 context: &mut Context,
240 input: &ProtoValue,
241 ) -> HandlerResult<StepDecision>;
242 fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions>;
243}
244
245struct TypedFlow<SomeFlow> {
246 flow: Arc<SomeFlow>,
247 timeout_handler: Option<crate::FlowTimeoutHandler<SomeFlow>>,
248}
249
250impl<SomeFlow: Flow> ErasedFlow for TypedFlow<SomeFlow> {
251 fn has_timeout_handler(&self) -> bool {
252 self.timeout_handler.is_some()
253 }
254
255 fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision> {
256 let handler = self.timeout_handler.ok_or_else(|| {
257 crate::HandlerError::new("dex_sdk::HandlerError", "Flow has no timeout handler")
258 })?;
259 handler(self.flow.as_ref(), context)
260 }
261 fn wait_for(
262 &self,
263 step_type: &str,
264 context: &mut Context,
265 input: &ProtoValue,
266 ) -> HandlerResult<Wait> {
267 self.flow
268 .steps()
269 .find(step_type)
270 .ok_or_else(|| {
271 crate::HandlerError::new(
272 "dex_sdk::HandlerError",
273 format!("Step is not registered: {step_type}"),
274 )
275 })?
276 .wait_for(context, input)
277 }
278
279 fn execute(
280 &self,
281 step_type: &str,
282 context: &mut Context,
283 input: &ProtoValue,
284 ) -> HandlerResult<StepDecision> {
285 self.flow
286 .steps()
287 .find(step_type)
288 .ok_or_else(|| {
289 crate::HandlerError::new(
290 "dex_sdk::HandlerError",
291 format!("Step is not registered: {step_type}"),
292 )
293 })?
294 .execute(context, input)
295 }
296
297 fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions> {
298 Ok(self
299 .flow
300 .steps()
301 .find(step_type)
302 .ok_or_else(|| {
303 crate::HandlerError::new(
304 "dex_sdk::HandlerError",
305 format!("Step is not registered: {step_type}"),
306 )
307 })?
308 .options())
309 }
310}
311
312fn assemble_persistence(
313 flow_name: &str,
314 definitions: &[PersistenceDefinition],
315) -> SdkResult<HashMap<String, PersistenceDefinition>> {
316 let mut persistence = HashMap::new();
317 for definition in definitions {
318 if definition.name.is_empty() {
319 return Err(definition_error(format!(
320 "Flow {flow_name} has an empty persistence definition name"
321 )));
322 }
323 if matches!(
324 definition.kind,
325 PersistenceKind::Attribute
326 | PersistenceKind::AttributeMap
327 | PersistenceKind::Channel
328 | PersistenceKind::ChannelMap
329 ) && definition.name.contains('/')
330 {
331 return Err(definition_error(format!(
332 "Flow {flow_name} persistence definition {} must not contain /",
333 definition.name
334 )));
335 }
336 if persistence
337 .insert(definition.name.clone(), definition.clone())
338 .is_some()
339 {
340 return Err(definition_error(format!(
341 "Flow {flow_name} has duplicate persistence definition {}",
342 definition.name
343 )));
344 }
345 }
346 Ok(persistence)
347}
348
349fn validate_rpc(
350 flow_name: &str,
351 rpc: &RegisteredRpc,
352 persistence: &HashMap<String, PersistenceDefinition>,
353) -> SdkResult<()> {
354 if rpc.timeout.is_some_and(|timeout| timeout.is_zero()) {
355 return Err(definition_error(format!(
356 "Flow {flow_name} RPC {} timeout must be positive",
357 rpc.name
358 )));
359 }
360 let mut locks = HashSet::new();
361 for lock in &rpc.locks {
362 let physical_name = lock.physical_name();
363 let logical_name = physical_name.split('/').next().unwrap_or(&physical_name);
364 let definition = persistence.get(logical_name).ok_or_else(|| {
365 definition_error(format!(
366 "Flow {flow_name} RPC {} locks an unregistered Attribute",
367 rpc.name
368 ))
369 })?;
370 if !matches!(
371 definition.kind,
372 PersistenceKind::Attribute | PersistenceKind::AttributeMap
373 ) {
374 return Err(definition_error(format!(
375 "Flow {flow_name} RPC {} locks a non-Attribute",
376 rpc.name
377 )));
378 }
379 if !locks.insert(physical_name) {
380 return Err(definition_error(format!(
381 "Flow {flow_name} RPC {} has a duplicate lock",
382 rpc.name
383 )));
384 }
385 }
386 validate_map_loads(
387 flow_name,
388 &format!("RPC {}", rpc.name),
389 &rpc.load_attribute_maps,
390 PersistenceKind::AttributeMap,
391 persistence,
392 |load| (&load.name, load.instance.as_deref()),
393 )?;
394 validate_definition_loads(
395 flow_name,
396 &format!("RPC {}", rpc.name),
397 &rpc.load_channels,
398 PersistenceKind::Channel,
399 persistence,
400 |load| &load.name,
401 )?;
402 validate_map_loads(
403 flow_name,
404 &format!("RPC {}", rpc.name),
405 &rpc.load_channel_maps,
406 PersistenceKind::ChannelMap,
407 persistence,
408 |load| (&load.name, load.instance.as_deref()),
409 )?;
410 Ok(())
411}
412
413fn validate_step_state_loads(
414 flow_name: &str,
415 step_name: &str,
416 options: &ErasedStepOptions,
417 persistence: &HashMap<String, PersistenceDefinition>,
418) -> SdkResult<()> {
419 validate_step_method_state_loads(
420 flow_name,
421 &format!("Step {step_name} WaitFor"),
422 &options.wait_for_load_attribute_maps,
423 &options.wait_for_load_channels,
424 &options.wait_for_load_channel_maps,
425 persistence,
426 )?;
427 validate_step_method_state_loads(
428 flow_name,
429 &format!("Step {step_name} Execute"),
430 &options.execute_load_attribute_maps,
431 &options.execute_load_channels,
432 &options.execute_load_channel_maps,
433 persistence,
434 )?;
435 Ok(())
436}
437
438fn validate_step_method_state_loads(
439 flow_name: &str,
440 source: &str,
441 attribute_maps: &[crate::AttributeMapLoad],
442 channels: &[crate::channel::ChannelLoad],
443 channel_maps: &[crate::ChannelMapLoad],
444 persistence: &HashMap<String, PersistenceDefinition>,
445) -> SdkResult<()> {
446 validate_map_loads(
447 flow_name,
448 source,
449 attribute_maps,
450 PersistenceKind::AttributeMap,
451 persistence,
452 |load| (&load.name, load.instance.as_deref()),
453 )?;
454 validate_definition_loads(
455 flow_name,
456 source,
457 channels,
458 PersistenceKind::Channel,
459 persistence,
460 |load| &load.name,
461 )?;
462 validate_map_loads(
463 flow_name,
464 source,
465 channel_maps,
466 PersistenceKind::ChannelMap,
467 persistence,
468 |load| (&load.name, load.instance.as_deref()),
469 )?;
470 Ok(())
471}
472
473fn validate_map_loads<Load, GetLoad>(
474 flow_name: &str,
475 source: &str,
476 loads: &[Load],
477 kind: PersistenceKind,
478 persistence: &HashMap<String, PersistenceDefinition>,
479 get_load: GetLoad,
480) -> SdkResult<()>
481where
482 GetLoad: for<'a> Fn(&'a Load) -> (&'a String, Option<&'a str>),
483{
484 let mut physical_names = HashSet::new();
485 for load in loads {
486 let (name, instance) = get_load(load);
487 validate_load_definition(flow_name, source, name, kind, persistence)?;
488 if instance.is_some_and(str::is_empty) {
489 return Err(definition_error(format!(
490 "Flow {flow_name} {source} loads an empty map instance"
491 )));
492 }
493 if instance.is_some_and(|value| value.contains('/')) {
494 return Err(definition_error(format!(
495 "Flow {flow_name} {source} map instance must not contain /"
496 )));
497 }
498 let physical_name = match instance {
499 Some(instance) => physical_name(name, instance),
500 None => format!("{name}/"),
501 };
502 if !physical_names.insert(physical_name) {
503 return Err(definition_error(format!(
504 "Flow {flow_name} {source} has a duplicate state load"
505 )));
506 }
507 }
508 Ok(())
509}
510
511fn validate_definition_loads<Load, GetLoad>(
512 flow_name: &str,
513 source: &str,
514 loads: &[Load],
515 kind: PersistenceKind,
516 persistence: &HashMap<String, PersistenceDefinition>,
517 get_load: GetLoad,
518) -> SdkResult<()>
519where
520 GetLoad: for<'a> Fn(&'a Load) -> &'a String,
521{
522 let mut names = HashSet::new();
523 for load in loads {
524 let name = get_load(load);
525 validate_load_definition(flow_name, source, name, kind, persistence)?;
526 if !names.insert(name) {
527 return Err(definition_error(format!(
528 "Flow {flow_name} {source} has a duplicate state load"
529 )));
530 }
531 }
532 Ok(())
533}
534
535fn validate_load_definition(
536 flow_name: &str,
537 source: &str,
538 name: &str,
539 kind: PersistenceKind,
540 persistence: &HashMap<String, PersistenceDefinition>,
541) -> SdkResult<()> {
542 let definition = persistence.get(name).ok_or_else(|| {
543 definition_error(format!(
544 "Flow {flow_name} {source} loads unregistered state: {name}"
545 ))
546 })?;
547 if definition.kind != kind {
548 return Err(definition_error(format!(
549 "Flow {flow_name} {source} loads the wrong state kind: {name}"
550 )));
551 }
552 Ok(())
553}
554
555fn require_static_name(name: &'static str, kind: &str) -> SdkResult<&'static str> {
556 if name.is_empty() {
557 Err(definition_error(format!("{kind} is required")))
558 } else {
559 Ok(name)
560 }
561}
562
563fn definition_error(message: impl Into<String>) -> SdkError {
564 SdkError::FlowDefinition {
565 message: message.into(),
566 }
567}
568
569pub(crate) fn physical_name(name: &str, instance: &str) -> String {
570 assert_map_instance(instance);
571 let mut encoded = String::new();
572 for byte in instance.bytes() {
573 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
574 encoded.push(char::from(byte));
575 } else {
576 encoded.push_str(&format!("%{byte:02X}"));
577 }
578 }
579 format!("{name}/{encoded}")
580}
581
582pub(crate) fn decode_instance(physical_name: &str, prefix: &str) -> Result<String, String> {
583 let encoded = physical_name
584 .strip_prefix(prefix)
585 .ok_or_else(|| format!("map key {physical_name} does not start with {prefix}"))?;
586 let bytes = encoded.as_bytes();
587 let mut decoded = Vec::with_capacity(bytes.len());
588 let mut index = 0;
589 while index < bytes.len() {
590 if bytes[index] == b'%' {
591 if index + 2 >= bytes.len() {
592 return Err(format!("invalid percent encoding in {physical_name}"));
593 }
594 let high = hex_value(bytes[index + 1])?;
595 let low = hex_value(bytes[index + 2])?;
596 decoded.push(high * 16 + low);
597 index += 3;
598 } else {
599 decoded.push(bytes[index]);
600 index += 1;
601 }
602 }
603 let instance = String::from_utf8(decoded).map_err(|error| error.to_string())?;
604 validate_map_instance(&instance)?;
605 Ok(instance)
606}
607
608pub(crate) fn assert_map_instance(instance: &str) {
609 if let Err(error) = validate_map_instance(instance) {
610 panic!("{error}");
611 }
612}
613
614pub(crate) fn validate_map_instance(instance: &str) -> Result<(), String> {
615 if instance.is_empty() {
616 return Err("map instance is required".to_string());
617 }
618 if instance.contains('/') {
619 return Err("map instances must not contain '/'".to_string());
620 }
621 Ok(())
622}
623
624fn hex_value(byte: u8) -> Result<u8, String> {
625 match byte {
626 b'0'..=b'9' => Ok(byte - b'0'),
627 b'a'..=b'f' => Ok(byte - b'a' + 10),
628 b'A'..=b'F' => Ok(byte - b'A' + 10),
629 _ => Err(format!(
630 "invalid percent-encoding digit {}",
631 char::from(byte)
632 )),
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::{PersistenceDefinition, PersistenceKind, assemble_persistence, physical_name};
639
640 #[test]
641 fn persistence_definition_names_reserve_slash() {
642 for kind in [
643 PersistenceKind::Attribute,
644 PersistenceKind::AttributeMap,
645 PersistenceKind::Channel,
646 PersistenceKind::ChannelMap,
647 ] {
648 let result = assemble_persistence(
649 "TestFlow",
650 &[PersistenceDefinition {
651 name: "orders/by-id".to_string(),
652 kind,
653 index: None,
654 sync_to_attribute_store: false,
655 stream_identity: None,
656 stream_capacity_bytes: None,
657 }],
658 );
659 assert!(result.is_err());
660 }
661
662 assert!(std::panic::catch_unwind(|| physical_name("messages", "orders/by-id")).is_err());
663 }
664}