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