Skip to main content

dex_sdk/
registry.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use 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)]
21/// Validates and stores all Flow definitions used by a Client or Worker.
22///
23/// Registration checks unique Flow, Step, RPC, and persistence names; starting-Step constraints;
24/// Attribute index consistency; RPC locks; and handler options. Cloning is cheap because it shares
25/// the registered data.
26///
27/// # Examples
28///
29/// ```
30/// use dex_sdk::{Flow, Registry};
31///
32/// struct OrderFlow;
33/// impl Flow for OrderFlow { type StartInput = String; }
34///
35/// let registry = Registry::new().register(OrderFlow)?;
36/// # Ok::<(), dex_sdk::SdkError>(())
37/// ```
38pub 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    /// Creates an empty registry.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Validates and adds one owned Flow definition.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`SdkError::FlowDefinition`] for invalid or conflicting names, schemas, indexes,
71    /// Steps, RPCs, locks, or options.
72    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}