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 flow_types(&self) -> Vec<String> {
139 let mut flow_types = self
140 .inner
141 .flows
142 .keys()
143 .map(|name| (*name).to_string())
144 .collect::<Vec<_>>();
145 flow_types.sort();
146 flow_types
147 }
148
149 pub(crate) fn rpc_with_flow(&self, name: &str) -> SdkResult<(&RegisteredFlow, &RegisteredRpc)> {
150 let mut matched = None;
151 for flow in self.inner.flows.values() {
152 let Some(rpc) = flow.rpcs.get(name) else {
153 continue;
154 };
155 if matched.is_some() {
156 return Err(SdkError::FlowDefinition {
157 message: format!("RPC name is ambiguous across registered Flows: {name}"),
158 });
159 }
160 matched = Some((flow, rpc));
161 }
162 matched.ok_or_else(|| SdkError::FlowDefinition {
163 message: format!("RPC is not registered: {name}"),
164 })
165 }
166
167 pub(crate) fn flow_for_stream<T>(&self, stream: &Stream<T>) -> SdkResult<&RegisteredFlow> {
168 let flow_name = self
169 .inner
170 .stream_flows
171 .get(&stream.identity())
172 .ok_or_else(|| SdkError::FlowDefinition {
173 message: format!("Stream is not registered: {}", stream.name()),
174 })?;
175 self.flow(flow_name)
176 }
177
178 pub(crate) fn attribute_indexes(&self) -> &HashMap<String, i32> {
179 &self.inner.attribute_indexes
180 }
181}
182
183fn assemble_flow<SomeFlow: Flow>(
184 name: &'static str,
185 flow: Arc<SomeFlow>,
186) -> SdkResult<RegisteredFlow> {
187 let timeout_handler = flow.timeout_handler();
188 let mut steps = HashMap::new();
189 let mut start_step = None;
190 for step in flow.steps().into_definitions() {
191 require_static_name(step.name, "Step type")?;
192 if step.starting {
193 if start_step.is_some() {
194 return Err(definition_error(format!(
195 "Flow {name} must not have multiple start Steps"
196 )));
197 }
198 start_step = Some(step.clone());
199 }
200 if steps.insert(step.name, step).is_some() {
201 return Err(definition_error(format!(
202 "Flow {name} has a duplicate Step"
203 )));
204 }
205 }
206
207 let persistence = assemble_persistence(name, flow.persistence().definitions())?;
208 for step in steps.values() {
209 let options = flow
210 .steps()
211 .find(step.name)
212 .ok_or_else(|| definition_error(format!("Flow {name} Step disappeared")))?
213 .options();
214 validate_step_state_loads(name, step.name, &options, &persistence)?;
215 }
216 let mut rpcs = HashMap::new();
217 for rpc in flow.rpcs().bind(Arc::clone(&flow)) {
218 require_static_name(rpc.name, "RPC name")?;
219 validate_rpc(name, &rpc, &persistence)?;
220 if rpcs.insert(rpc.name, rpc).is_some() {
221 return Err(definition_error(format!("Flow {name} has a duplicate RPC")));
222 }
223 }
224 Ok(RegisteredFlow {
225 name,
226 type_id: TypeId::of::<SomeFlow>(),
227 steps,
228 start_step,
229 rpcs,
230 persistence,
231 handler: Arc::new(TypedFlow {
232 flow,
233 timeout_handler,
234 }),
235 })
236}
237
238pub(crate) trait ErasedFlow: Send + Sync {
239 fn has_timeout_handler(&self) -> bool;
240 fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision>;
241 fn wait_for(
242 &self,
243 step_type: &str,
244 context: &mut Context,
245 input: &ProtoValue,
246 ) -> HandlerResult<Wait>;
247 fn execute(
248 &self,
249 step_type: &str,
250 context: &mut Context,
251 input: &ProtoValue,
252 ) -> HandlerResult<StepDecision>;
253 fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions>;
254}
255
256struct TypedFlow<SomeFlow> {
257 flow: Arc<SomeFlow>,
258 timeout_handler: Option<crate::FlowTimeoutHandler<SomeFlow>>,
259}
260
261impl<SomeFlow: Flow> ErasedFlow for TypedFlow<SomeFlow> {
262 fn has_timeout_handler(&self) -> bool {
263 self.timeout_handler.is_some()
264 }
265
266 fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision> {
267 let handler = self.timeout_handler.ok_or_else(|| {
268 crate::HandlerError::new("dex_sdk::HandlerError", "Flow has no timeout handler")
269 })?;
270 handler(self.flow.as_ref(), context)
271 }
272 fn wait_for(
273 &self,
274 step_type: &str,
275 context: &mut Context,
276 input: &ProtoValue,
277 ) -> HandlerResult<Wait> {
278 self.flow
279 .steps()
280 .find(step_type)
281 .ok_or_else(|| {
282 crate::HandlerError::new(
283 "dex_sdk::HandlerError",
284 format!("Step is not registered: {step_type}"),
285 )
286 })?
287 .wait_for(context, input)
288 }
289
290 fn execute(
291 &self,
292 step_type: &str,
293 context: &mut Context,
294 input: &ProtoValue,
295 ) -> HandlerResult<StepDecision> {
296 self.flow
297 .steps()
298 .find(step_type)
299 .ok_or_else(|| {
300 crate::HandlerError::new(
301 "dex_sdk::HandlerError",
302 format!("Step is not registered: {step_type}"),
303 )
304 })?
305 .execute(context, input)
306 }
307
308 fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions> {
309 Ok(self
310 .flow
311 .steps()
312 .find(step_type)
313 .ok_or_else(|| {
314 crate::HandlerError::new(
315 "dex_sdk::HandlerError",
316 format!("Step is not registered: {step_type}"),
317 )
318 })?
319 .options())
320 }
321}
322
323fn assemble_persistence(
324 flow_name: &str,
325 definitions: &[PersistenceDefinition],
326) -> SdkResult<HashMap<String, PersistenceDefinition>> {
327 let mut persistence = HashMap::new();
328 for definition in definitions {
329 if definition.name.is_empty() {
330 return Err(definition_error(format!(
331 "Flow {flow_name} has an empty persistence definition name"
332 )));
333 }
334 if matches!(
335 definition.kind,
336 PersistenceKind::Attribute
337 | PersistenceKind::AttributeMap
338 | PersistenceKind::Channel
339 | PersistenceKind::ChannelMap
340 ) && definition.name.contains('/')
341 {
342 return Err(definition_error(format!(
343 "Flow {flow_name} persistence definition {} must not contain /",
344 definition.name
345 )));
346 }
347 validate_hard_delete_definition(flow_name, definition)?;
348 if persistence
349 .insert(definition.name.clone(), definition.clone())
350 .is_some()
351 {
352 return Err(definition_error(format!(
353 "Flow {flow_name} has duplicate persistence definition {}",
354 definition.name
355 )));
356 }
357 }
358 Ok(persistence)
359}
360
361fn validate_hard_delete_definition(
362 flow_name: &str,
363 definition: &PersistenceDefinition,
364) -> SdkResult<()> {
365 if !definition.is_hard_delete_on_null {
366 return Ok(());
367 }
368 let name = &definition.name;
369 if definition.index.is_some() {
370 return Err(definition_error(format!(
371 "Flow {flow_name} hard-delete Attribute {name} cannot be combined with an index"
372 )));
373 }
374 if definition.sync_to_attribute_store {
375 return Err(definition_error(format!(
376 "Flow {flow_name} hard-delete Attribute {name} cannot be combined with \
377 sync_to_attribute_store"
378 )));
379 }
380 if let Some(type_name) = definition.numeric_or_boolean_type_name {
381 return Err(definition_error(format!(
382 "Flow {flow_name} hard-delete Attribute {name} supports only string and object \
383 values, but its value type is {type_name}"
384 )));
385 }
386 Ok(())
387}
388
389fn validate_rpc(
390 flow_name: &str,
391 rpc: &RegisteredRpc,
392 persistence: &HashMap<String, PersistenceDefinition>,
393) -> SdkResult<()> {
394 if rpc.timeout.is_some_and(|timeout| timeout.is_zero()) {
395 return Err(definition_error(format!(
396 "Flow {flow_name} RPC {} timeout must be positive",
397 rpc.name
398 )));
399 }
400 let mut locks = HashSet::new();
401 for lock in &rpc.locks {
402 let physical_name = lock.physical_name();
403 let logical_name = physical_name.split('/').next().unwrap_or(&physical_name);
404 let definition = persistence.get(logical_name).ok_or_else(|| {
405 definition_error(format!(
406 "Flow {flow_name} RPC {} locks an unregistered Attribute",
407 rpc.name
408 ))
409 })?;
410 if !matches!(
411 definition.kind,
412 PersistenceKind::Attribute | PersistenceKind::AttributeMap
413 ) {
414 return Err(definition_error(format!(
415 "Flow {flow_name} RPC {} locks a non-Attribute",
416 rpc.name
417 )));
418 }
419 if !locks.insert(physical_name) {
420 return Err(definition_error(format!(
421 "Flow {flow_name} RPC {} has a duplicate lock",
422 rpc.name
423 )));
424 }
425 }
426 validate_map_loads(
427 flow_name,
428 &format!("RPC {}", rpc.name),
429 &rpc.load_attribute_maps,
430 PersistenceKind::AttributeMap,
431 persistence,
432 |load| (&load.name, load.instance.as_deref()),
433 )?;
434 validate_definition_loads(
435 flow_name,
436 &format!("RPC {}", rpc.name),
437 &rpc.load_channels,
438 PersistenceKind::Channel,
439 persistence,
440 |load| &load.name,
441 )?;
442 validate_map_loads(
443 flow_name,
444 &format!("RPC {}", rpc.name),
445 &rpc.load_channel_maps,
446 PersistenceKind::ChannelMap,
447 persistence,
448 |load| (&load.name, load.instance.as_deref()),
449 )?;
450 Ok(())
451}
452
453fn validate_step_state_loads(
454 flow_name: &str,
455 step_name: &str,
456 options: &ErasedStepOptions,
457 persistence: &HashMap<String, PersistenceDefinition>,
458) -> SdkResult<()> {
459 validate_step_method_state_loads(
460 flow_name,
461 &format!("Step {step_name} WaitFor"),
462 &options.wait_for_load_attribute_maps,
463 &options.wait_for_load_channels,
464 &options.wait_for_load_channel_maps,
465 persistence,
466 )?;
467 validate_step_method_state_loads(
468 flow_name,
469 &format!("Step {step_name} Execute"),
470 &options.execute_load_attribute_maps,
471 &options.execute_load_channels,
472 &options.execute_load_channel_maps,
473 persistence,
474 )?;
475 Ok(())
476}
477
478fn validate_step_method_state_loads(
479 flow_name: &str,
480 source: &str,
481 attribute_maps: &[crate::AttributeMapLoad],
482 channels: &[crate::channel::ChannelLoad],
483 channel_maps: &[crate::ChannelMapLoad],
484 persistence: &HashMap<String, PersistenceDefinition>,
485) -> SdkResult<()> {
486 validate_map_loads(
487 flow_name,
488 source,
489 attribute_maps,
490 PersistenceKind::AttributeMap,
491 persistence,
492 |load| (&load.name, load.instance.as_deref()),
493 )?;
494 validate_definition_loads(
495 flow_name,
496 source,
497 channels,
498 PersistenceKind::Channel,
499 persistence,
500 |load| &load.name,
501 )?;
502 validate_map_loads(
503 flow_name,
504 source,
505 channel_maps,
506 PersistenceKind::ChannelMap,
507 persistence,
508 |load| (&load.name, load.instance.as_deref()),
509 )?;
510 Ok(())
511}
512
513fn validate_map_loads<Load, GetLoad>(
514 flow_name: &str,
515 source: &str,
516 loads: &[Load],
517 kind: PersistenceKind,
518 persistence: &HashMap<String, PersistenceDefinition>,
519 get_load: GetLoad,
520) -> SdkResult<()>
521where
522 GetLoad: for<'a> Fn(&'a Load) -> (&'a String, Option<&'a str>),
523{
524 let mut physical_names = HashSet::new();
525 for load in loads {
526 let (name, instance) = get_load(load);
527 validate_load_definition(flow_name, source, name, kind, persistence)?;
528 if instance.is_some_and(str::is_empty) {
529 return Err(definition_error(format!(
530 "Flow {flow_name} {source} loads an empty map instance"
531 )));
532 }
533 if instance.is_some_and(|value| value.contains('/')) {
534 return Err(definition_error(format!(
535 "Flow {flow_name} {source} map instance must not contain /"
536 )));
537 }
538 let physical_name = match instance {
539 Some(instance) => physical_name(name, instance),
540 None => format!("{name}/"),
541 };
542 if !physical_names.insert(physical_name) {
543 return Err(definition_error(format!(
544 "Flow {flow_name} {source} has a duplicate state load"
545 )));
546 }
547 }
548 Ok(())
549}
550
551fn validate_definition_loads<Load, GetLoad>(
552 flow_name: &str,
553 source: &str,
554 loads: &[Load],
555 kind: PersistenceKind,
556 persistence: &HashMap<String, PersistenceDefinition>,
557 get_load: GetLoad,
558) -> SdkResult<()>
559where
560 GetLoad: for<'a> Fn(&'a Load) -> &'a String,
561{
562 let mut names = HashSet::new();
563 for load in loads {
564 let name = get_load(load);
565 validate_load_definition(flow_name, source, name, kind, persistence)?;
566 if !names.insert(name) {
567 return Err(definition_error(format!(
568 "Flow {flow_name} {source} has a duplicate state load"
569 )));
570 }
571 }
572 Ok(())
573}
574
575fn validate_load_definition(
576 flow_name: &str,
577 source: &str,
578 name: &str,
579 kind: PersistenceKind,
580 persistence: &HashMap<String, PersistenceDefinition>,
581) -> SdkResult<()> {
582 let definition = persistence.get(name).ok_or_else(|| {
583 definition_error(format!(
584 "Flow {flow_name} {source} loads unregistered state: {name}"
585 ))
586 })?;
587 if definition.kind != kind {
588 return Err(definition_error(format!(
589 "Flow {flow_name} {source} loads the wrong state kind: {name}"
590 )));
591 }
592 Ok(())
593}
594
595fn require_static_name(name: &'static str, kind: &str) -> SdkResult<&'static str> {
596 if name.is_empty() {
597 Err(definition_error(format!("{kind} is required")))
598 } else {
599 Ok(name)
600 }
601}
602
603fn definition_error(message: impl Into<String>) -> SdkError {
604 SdkError::FlowDefinition {
605 message: message.into(),
606 }
607}
608
609pub(crate) fn physical_name(name: &str, instance: &str) -> String {
610 assert_map_instance(instance);
611 let mut encoded = String::new();
612 for byte in instance.bytes() {
613 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
614 encoded.push(char::from(byte));
615 } else {
616 encoded.push_str(&format!("%{byte:02X}"));
617 }
618 }
619 format!("{name}/{encoded}")
620}
621
622pub(crate) fn decode_instance(physical_name: &str, prefix: &str) -> Result<String, String> {
623 let encoded = physical_name
624 .strip_prefix(prefix)
625 .ok_or_else(|| format!("map key {physical_name} does not start with {prefix}"))?;
626 let bytes = encoded.as_bytes();
627 let mut decoded = Vec::with_capacity(bytes.len());
628 let mut index = 0;
629 while index < bytes.len() {
630 if bytes[index] == b'%' {
631 if index + 2 >= bytes.len() {
632 return Err(format!("invalid percent encoding in {physical_name}"));
633 }
634 let high = hex_value(bytes[index + 1])?;
635 let low = hex_value(bytes[index + 2])?;
636 decoded.push(high * 16 + low);
637 index += 3;
638 } else {
639 decoded.push(bytes[index]);
640 index += 1;
641 }
642 }
643 let instance = String::from_utf8(decoded).map_err(|error| error.to_string())?;
644 validate_map_instance(&instance)?;
645 Ok(instance)
646}
647
648pub(crate) fn assert_map_instance(instance: &str) {
649 if let Err(error) = validate_map_instance(instance) {
650 panic!("{error}");
651 }
652}
653
654pub(crate) fn validate_map_instance(instance: &str) -> Result<(), String> {
655 if instance.is_empty() {
656 return Err("map instance is required".to_string());
657 }
658 if instance.contains('/') {
659 return Err("map instances must not contain '/'".to_string());
660 }
661 Ok(())
662}
663
664fn hex_value(byte: u8) -> Result<u8, String> {
665 match byte {
666 b'0'..=b'9' => Ok(byte - b'0'),
667 b'a'..=b'f' => Ok(byte - b'a' + 10),
668 b'A'..=b'F' => Ok(byte - b'A' + 10),
669 _ => Err(format!(
670 "invalid percent-encoding digit {}",
671 char::from(byte)
672 )),
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::{PersistenceDefinition, PersistenceKind, assemble_persistence, physical_name};
679 use crate::{Attribute, AttributeIndex, AttributeMap, PersistenceSchema, SdkError};
680
681 #[test]
682 fn persistence_definition_names_reserve_slash() {
683 for kind in [
684 PersistenceKind::Attribute,
685 PersistenceKind::AttributeMap,
686 PersistenceKind::Channel,
687 PersistenceKind::ChannelMap,
688 ] {
689 let result = assemble_persistence(
690 "TestFlow",
691 &[PersistenceDefinition::new("orders/by-id", kind)],
692 );
693 assert!(result.is_err());
694 }
695
696 assert!(std::panic::catch_unwind(|| physical_name("messages", "orders/by-id")).is_err());
697 }
698
699 #[test]
700 fn hard_delete_definitions_reject_index_sync_and_scalar_types() {
701 let indexed = PersistenceSchema::new().attribute(
702 &Attribute::<String>::new("secret")
703 .indexed(AttributeIndex::keyword())
704 .hard_delete_on_null(),
705 );
706 let synced = PersistenceSchema::new().attribute_map(
707 &AttributeMap::<String>::new("secrets")
708 .hard_delete_on_null()
709 .sync_to_attribute_store(),
710 );
711 let boolean = PersistenceSchema::new()
712 .attribute(&Attribute::<Option<bool>>::new("flag").hard_delete_on_null());
713 let floating = PersistenceSchema::new()
714 .attribute_map(&AttributeMap::<f64>::new("ratios").hard_delete_on_null());
715 for (schema, expected) in [
716 (indexed, "cannot be combined with an index"),
717 (synced, "cannot be combined with sync_to_attribute_store"),
718 (boolean, "supports only string and object values"),
719 (floating, "supports only string and object values"),
720 ] {
721 let error = assemble_persistence("TestFlow", schema.definitions())
722 .expect_err("invalid hard-delete definition");
723 let SdkError::FlowDefinition { message } = error else {
724 panic!("unexpected error {error}");
725 };
726 assert!(message.contains(expected), "{message}");
727 }
728
729 let valid = PersistenceSchema::new()
730 .attribute(&Attribute::<String>::new("secret").hard_delete_on_null())
731 .attribute(&Attribute::<Vec<u8>>::new("bytes").hard_delete_on_null())
732 .attribute_map(
733 &AttributeMap::<std::collections::HashMap<String, u64>>::new("tokens")
734 .hard_delete_on_null(),
735 );
736 let persistence =
737 assemble_persistence("TestFlow", valid.definitions()).expect("valid definitions");
738 assert!(persistence["secret"].is_hard_delete_on_null);
739 }
740}