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 validate_rpc_map_loads(
379 flow_name,
380 rpc.name,
381 &rpc.load_attribute_maps,
382 PersistenceKind::AttributeMap,
383 persistence,
384 |load| (&load.name, load.instance.as_deref()),
385 )?;
386 validate_rpc_definition_loads(
387 flow_name,
388 rpc.name,
389 &rpc.load_channels,
390 PersistenceKind::Channel,
391 persistence,
392 |load| &load.name,
393 )?;
394 validate_rpc_map_loads(
395 flow_name,
396 rpc.name,
397 &rpc.load_channel_maps,
398 PersistenceKind::ChannelMap,
399 persistence,
400 |load| (&load.name, load.instance.as_deref()),
401 )?;
402 Ok(())
403}
404
405fn validate_rpc_map_loads<Load, GetLoad>(
406 flow_name: &str,
407 rpc_name: &str,
408 loads: &[Load],
409 kind: PersistenceKind,
410 persistence: &HashMap<String, PersistenceDefinition>,
411 get_load: GetLoad,
412) -> SdkResult<()>
413where
414 GetLoad: for<'a> Fn(&'a Load) -> (&'a String, Option<&'a str>),
415{
416 let mut physical_names = HashSet::new();
417 for load in loads {
418 let (name, instance) = get_load(load);
419 validate_rpc_load_definition(flow_name, rpc_name, name, kind, persistence)?;
420 if instance.is_some_and(str::is_empty) {
421 return Err(definition_error(format!(
422 "Flow {flow_name} RPC {rpc_name} loads an empty map instance"
423 )));
424 }
425 if instance.is_some_and(|value| value.contains('/')) {
426 return Err(definition_error(format!(
427 "Flow {flow_name} RPC {rpc_name} map instance must not contain /"
428 )));
429 }
430 let physical_name = match instance {
431 Some(instance) => physical_name(name, instance),
432 None => format!("{name}/"),
433 };
434 if !physical_names.insert(physical_name) {
435 return Err(definition_error(format!(
436 "Flow {flow_name} RPC {rpc_name} has a duplicate state load"
437 )));
438 }
439 }
440 Ok(())
441}
442
443fn validate_rpc_definition_loads<Load, GetLoad>(
444 flow_name: &str,
445 rpc_name: &str,
446 loads: &[Load],
447 kind: PersistenceKind,
448 persistence: &HashMap<String, PersistenceDefinition>,
449 get_load: GetLoad,
450) -> SdkResult<()>
451where
452 GetLoad: for<'a> Fn(&'a Load) -> &'a String,
453{
454 let mut names = HashSet::new();
455 for load in loads {
456 let name = get_load(load);
457 validate_rpc_load_definition(flow_name, rpc_name, name, kind, persistence)?;
458 if !names.insert(name) {
459 return Err(definition_error(format!(
460 "Flow {flow_name} RPC {rpc_name} has a duplicate state load"
461 )));
462 }
463 }
464 Ok(())
465}
466
467fn validate_rpc_load_definition(
468 flow_name: &str,
469 rpc_name: &str,
470 name: &str,
471 kind: PersistenceKind,
472 persistence: &HashMap<String, PersistenceDefinition>,
473) -> SdkResult<()> {
474 let definition = persistence.get(name).ok_or_else(|| {
475 definition_error(format!(
476 "Flow {flow_name} RPC {rpc_name} loads unregistered state: {name}"
477 ))
478 })?;
479 if definition.kind != kind {
480 return Err(definition_error(format!(
481 "Flow {flow_name} RPC {rpc_name} loads the wrong state kind: {name}"
482 )));
483 }
484 Ok(())
485}
486
487fn require_static_name(name: &'static str, kind: &str) -> SdkResult<&'static str> {
488 if name.is_empty() {
489 Err(definition_error(format!("{kind} is required")))
490 } else {
491 Ok(name)
492 }
493}
494
495fn definition_error(message: impl Into<String>) -> SdkError {
496 SdkError::FlowDefinition {
497 message: message.into(),
498 }
499}
500
501pub(crate) fn physical_name(name: &str, instance: &str) -> String {
502 assert_map_instance(instance);
503 let mut encoded = String::new();
504 for byte in instance.bytes() {
505 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
506 encoded.push(char::from(byte));
507 } else {
508 encoded.push_str(&format!("%{byte:02X}"));
509 }
510 }
511 format!("{name}/{encoded}")
512}
513
514pub(crate) fn decode_instance(physical_name: &str, prefix: &str) -> Result<String, String> {
515 let encoded = physical_name
516 .strip_prefix(prefix)
517 .ok_or_else(|| format!("map key {physical_name} does not start with {prefix}"))?;
518 let bytes = encoded.as_bytes();
519 let mut decoded = Vec::with_capacity(bytes.len());
520 let mut index = 0;
521 while index < bytes.len() {
522 if bytes[index] == b'%' {
523 if index + 2 >= bytes.len() {
524 return Err(format!("invalid percent encoding in {physical_name}"));
525 }
526 let high = hex_value(bytes[index + 1])?;
527 let low = hex_value(bytes[index + 2])?;
528 decoded.push(high * 16 + low);
529 index += 3;
530 } else {
531 decoded.push(bytes[index]);
532 index += 1;
533 }
534 }
535 let instance = String::from_utf8(decoded).map_err(|error| error.to_string())?;
536 validate_map_instance(&instance)?;
537 Ok(instance)
538}
539
540pub(crate) fn assert_map_instance(instance: &str) {
541 if let Err(error) = validate_map_instance(instance) {
542 panic!("{error}");
543 }
544}
545
546pub(crate) fn validate_map_instance(instance: &str) -> Result<(), String> {
547 if instance.is_empty() {
548 return Err("map instance is required".to_string());
549 }
550 if instance.contains('/') {
551 return Err("map instances must not contain '/'".to_string());
552 }
553 Ok(())
554}
555
556fn hex_value(byte: u8) -> Result<u8, String> {
557 match byte {
558 b'0'..=b'9' => Ok(byte - b'0'),
559 b'a'..=b'f' => Ok(byte - b'a' + 10),
560 b'A'..=b'F' => Ok(byte - b'A' + 10),
561 _ => Err(format!(
562 "invalid percent-encoding digit {}",
563 char::from(byte)
564 )),
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::{PersistenceDefinition, PersistenceKind, assemble_persistence, physical_name};
571
572 #[test]
573 fn persistence_definition_names_reserve_slash() {
574 for kind in [
575 PersistenceKind::Attribute,
576 PersistenceKind::AttributeMap,
577 PersistenceKind::Channel,
578 PersistenceKind::ChannelMap,
579 ] {
580 let result = assemble_persistence(
581 "TestFlow",
582 &[PersistenceDefinition {
583 name: "orders/by-id".to_string(),
584 kind,
585 index: None,
586 sync_to_attribute_store: false,
587 stream_identity: None,
588 stream_capacity_bytes: None,
589 }],
590 );
591 assert!(result.is_err());
592 }
593
594 assert!(std::panic::catch_unwind(|| physical_name("messages", "orders/by-id")).is_err());
595 }
596}