Skip to main content

dex_sdk/
registry.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Super Durable Source 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-Super-Durable-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, 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}
47
48#[derive(Clone)]
49pub(crate) struct RegisteredFlow {
50    pub(crate) name: &'static str,
51    pub(crate) type_id: TypeId,
52    pub(crate) steps: HashMap<&'static str, RegisteredStep>,
53    pub(crate) start_step: Option<RegisteredStep>,
54    pub(crate) rpcs: HashMap<&'static str, RegisteredRpc>,
55    pub(crate) persistence: HashMap<String, PersistenceDefinition>,
56    pub(crate) handler: Arc<dyn ErasedFlow>,
57}
58
59impl Registry {
60    /// Creates an empty registry.
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Validates and adds one owned Flow definition.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`SdkError::FlowDefinition`] for invalid or conflicting names, schemas, indexes,
70    /// Steps, RPCs, locks, or options.
71    pub fn register<SomeFlow: Flow>(mut self, flow: SomeFlow) -> SdkResult<Self> {
72        let flow = Arc::new(flow);
73        let name = require_static_name(flow.flow_type(), "Flow type")?;
74        let registered = assemble_flow(name, Arc::clone(&flow))?;
75        let inner = Arc::make_mut(&mut self.inner);
76        let mut additions = Vec::new();
77        for definition in registered.persistence.values() {
78            let Some(index) = &definition.index else {
79                continue;
80            };
81            if definition.kind == PersistenceKind::AttributeMap
82                && index.key().is_none_or(str::is_empty)
83            {
84                return Err(definition_error(format!(
85                    "Flow {name} indexed AttributeMap {} requires an index key",
86                    definition.name
87                )));
88            }
89            let key = index.key().unwrap_or(&definition.name).to_string();
90            let index_type = index.proto_value();
91            if inner
92                .attribute_indexes
93                .get(&key)
94                .is_some_and(|existing| *existing != index_type)
95            {
96                return Err(definition_error(format!(
97                    "Attribute index {key} has conflicting types"
98                )));
99            }
100            additions.push((key, index_type));
101        }
102        if inner.flows.insert(name, registered).is_some() {
103            return Err(definition_error(format!("duplicate Flow {name}")));
104        }
105        inner.attribute_indexes.extend(additions);
106        Ok(self)
107    }
108
109    pub(crate) fn flow(&self, name: &str) -> SdkResult<&RegisteredFlow> {
110        self.inner
111            .flows
112            .get(name)
113            .ok_or_else(|| SdkError::FlowDefinition {
114                message: format!("Flow is not registered: {name}"),
115            })
116    }
117
118    pub(crate) fn rpc(&self, name: &str) -> SdkResult<&RegisteredRpc> {
119        let mut matched = None;
120        for flow in self.inner.flows.values() {
121            let Some(rpc) = flow.rpcs.get(name) else {
122                continue;
123            };
124            if matched.is_some() {
125                return Err(SdkError::FlowDefinition {
126                    message: format!("RPC name is ambiguous across registered Flows: {name}"),
127                });
128            }
129            matched = Some(rpc);
130        }
131        matched.ok_or_else(|| SdkError::FlowDefinition {
132            message: format!("RPC is not registered: {name}"),
133        })
134    }
135
136    pub(crate) fn attribute_indexes(&self) -> &HashMap<String, i32> {
137        &self.inner.attribute_indexes
138    }
139}
140
141fn assemble_flow<SomeFlow: Flow>(
142    name: &'static str,
143    flow: Arc<SomeFlow>,
144) -> SdkResult<RegisteredFlow> {
145    let timeout_handler = flow.timeout_handler();
146    let mut steps = HashMap::new();
147    let mut start_step = None;
148    for step in flow.steps().into_definitions() {
149        require_static_name(step.name, "Step type")?;
150        if step.starting {
151            if start_step.is_some() {
152                return Err(definition_error(format!(
153                    "Flow {name} must not have multiple start Steps"
154                )));
155            }
156            start_step = Some(step.clone());
157        }
158        if steps.insert(step.name, step).is_some() {
159            return Err(definition_error(format!(
160                "Flow {name} has a duplicate Step"
161            )));
162        }
163    }
164
165    let persistence = assemble_persistence(name, flow.persistence().definitions())?;
166    let mut rpcs = HashMap::new();
167    for rpc in flow.rpcs().bind(Arc::clone(&flow)) {
168        require_static_name(rpc.name, "RPC name")?;
169        validate_rpc(name, &rpc, &persistence)?;
170        if rpcs.insert(rpc.name, rpc).is_some() {
171            return Err(definition_error(format!("Flow {name} has a duplicate RPC")));
172        }
173    }
174    Ok(RegisteredFlow {
175        name,
176        type_id: TypeId::of::<SomeFlow>(),
177        steps,
178        start_step,
179        rpcs,
180        persistence,
181        handler: Arc::new(TypedFlow {
182            flow,
183            timeout_handler,
184        }),
185    })
186}
187
188pub(crate) trait ErasedFlow: Send + Sync {
189    fn has_timeout_handler(&self) -> bool;
190    fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision>;
191    fn wait_for(
192        &self,
193        step_type: &str,
194        context: &mut Context,
195        input: &ProtoValue,
196    ) -> HandlerResult<Wait>;
197    fn execute(
198        &self,
199        step_type: &str,
200        context: &mut Context,
201        input: &ProtoValue,
202    ) -> HandlerResult<StepDecision>;
203    fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions>;
204}
205
206struct TypedFlow<SomeFlow> {
207    flow: Arc<SomeFlow>,
208    timeout_handler: Option<crate::FlowTimeoutHandler<SomeFlow>>,
209}
210
211impl<SomeFlow: Flow> ErasedFlow for TypedFlow<SomeFlow> {
212    fn has_timeout_handler(&self) -> bool {
213        self.timeout_handler.is_some()
214    }
215
216    fn handle_timeout(&self, context: &mut Context) -> HandlerResult<StepDecision> {
217        let handler = self.timeout_handler.ok_or_else(|| {
218            crate::HandlerError::new("dex_sdk::HandlerError", "Flow has no timeout handler")
219        })?;
220        handler(self.flow.as_ref(), context)
221    }
222    fn wait_for(
223        &self,
224        step_type: &str,
225        context: &mut Context,
226        input: &ProtoValue,
227    ) -> HandlerResult<Wait> {
228        self.flow
229            .steps()
230            .find(step_type)
231            .ok_or_else(|| {
232                crate::HandlerError::new(
233                    "dex_sdk::HandlerError",
234                    format!("Step is not registered: {step_type}"),
235                )
236            })?
237            .wait_for(context, input)
238    }
239
240    fn execute(
241        &self,
242        step_type: &str,
243        context: &mut Context,
244        input: &ProtoValue,
245    ) -> HandlerResult<StepDecision> {
246        self.flow
247            .steps()
248            .find(step_type)
249            .ok_or_else(|| {
250                crate::HandlerError::new(
251                    "dex_sdk::HandlerError",
252                    format!("Step is not registered: {step_type}"),
253                )
254            })?
255            .execute(context, input)
256    }
257
258    fn step_options(&self, step_type: &str) -> HandlerResult<ErasedStepOptions> {
259        Ok(self
260            .flow
261            .steps()
262            .find(step_type)
263            .ok_or_else(|| {
264                crate::HandlerError::new(
265                    "dex_sdk::HandlerError",
266                    format!("Step is not registered: {step_type}"),
267                )
268            })?
269            .options())
270    }
271}
272
273fn assemble_persistence(
274    flow_name: &str,
275    definitions: &[PersistenceDefinition],
276) -> SdkResult<HashMap<String, PersistenceDefinition>> {
277    let mut persistence = HashMap::new();
278    for definition in definitions {
279        if definition.name.is_empty() {
280            return Err(definition_error(format!(
281                "Flow {flow_name} has an empty persistence definition name"
282            )));
283        }
284        if persistence
285            .insert(definition.name.clone(), definition.clone())
286            .is_some()
287        {
288            return Err(definition_error(format!(
289                "Flow {flow_name} has duplicate persistence definition {}",
290                definition.name
291            )));
292        }
293    }
294    Ok(persistence)
295}
296
297fn validate_rpc(
298    flow_name: &str,
299    rpc: &RegisteredRpc,
300    persistence: &HashMap<String, PersistenceDefinition>,
301) -> SdkResult<()> {
302    if rpc.timeout.is_some_and(|timeout| timeout.is_zero()) {
303        return Err(definition_error(format!(
304            "Flow {flow_name} RPC {} timeout must be positive",
305            rpc.name
306        )));
307    }
308    let mut locks = HashSet::new();
309    for lock in &rpc.locks {
310        let physical_name = lock.physical_name();
311        let logical_name = physical_name.split('/').next().unwrap_or(&physical_name);
312        let definition = persistence.get(logical_name).ok_or_else(|| {
313            definition_error(format!(
314                "Flow {flow_name} RPC {} locks an unregistered Attribute",
315                rpc.name
316            ))
317        })?;
318        if !matches!(
319            definition.kind,
320            PersistenceKind::Attribute | PersistenceKind::AttributeMap
321        ) {
322            return Err(definition_error(format!(
323                "Flow {flow_name} RPC {} locks a non-Attribute",
324                rpc.name
325            )));
326        }
327        if !locks.insert(physical_name) {
328            return Err(definition_error(format!(
329                "Flow {flow_name} RPC {} has a duplicate lock",
330                rpc.name
331            )));
332        }
333    }
334    Ok(())
335}
336
337fn require_static_name(name: &'static str, kind: &str) -> SdkResult<&'static str> {
338    if name.is_empty() {
339        Err(definition_error(format!("{kind} is required")))
340    } else {
341        Ok(name)
342    }
343}
344
345fn definition_error(message: impl Into<String>) -> SdkError {
346    SdkError::FlowDefinition {
347        message: message.into(),
348    }
349}
350
351pub(crate) fn physical_name(name: &str, instance: &str) -> String {
352    if instance.is_empty() {
353        panic!("persistence instance is required");
354    }
355    let mut encoded = String::new();
356    for byte in instance.bytes() {
357        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
358            encoded.push(char::from(byte));
359        } else {
360            encoded.push_str(&format!("%{byte:02X}"));
361        }
362    }
363    format!("{name}/{encoded}")
364}
365
366pub(crate) fn decode_instance(physical_name: &str, prefix: &str) -> Result<String, String> {
367    let encoded = physical_name
368        .strip_prefix(prefix)
369        .ok_or_else(|| format!("map key {physical_name} does not start with {prefix}"))?;
370    let bytes = encoded.as_bytes();
371    let mut decoded = Vec::with_capacity(bytes.len());
372    let mut index = 0;
373    while index < bytes.len() {
374        if bytes[index] == b'%' {
375            if index + 2 >= bytes.len() {
376                return Err(format!("invalid percent encoding in {physical_name}"));
377            }
378            let high = hex_value(bytes[index + 1])?;
379            let low = hex_value(bytes[index + 2])?;
380            decoded.push(high * 16 + low);
381            index += 3;
382        } else {
383            decoded.push(bytes[index]);
384            index += 1;
385        }
386    }
387    String::from_utf8(decoded).map_err(|error| error.to_string())
388}
389
390fn hex_value(byte: u8) -> Result<u8, String> {
391    match byte {
392        b'0'..=b'9' => Ok(byte - b'0'),
393        b'a'..=b'f' => Ok(byte - b'a' + 10),
394        b'A'..=b'F' => Ok(byte - b'A' + 10),
395        _ => Err(format!(
396            "invalid percent-encoding digit {}",
397            char::from(byte)
398        )),
399    }
400}